diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs
new file mode 100644
index 0000000..508c98e
--- /dev/null
+++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs
@@ -0,0 +1,41 @@
+using Thalos.Bff.Application.Contracts;
+using Thalos.Bff.Contracts.Api;
+using Thalos.Service.Identity.Abstractions.Contracts;
+
+namespace Thalos.Bff.Application.Adapters;
+
+///
+/// Default adapter implementation for identity edge API and service contracts.
+///
+public sealed class IdentityEdgeContractAdapter : IIdentityEdgeContractAdapter
+{
+ ///
+ public EvaluateIdentityPolicyRequest ToPolicyRequest(IssueTokenApiRequest request, string permissionCode)
+ {
+ return new EvaluateIdentityPolicyRequest(request.SubjectId, request.TenantId, permissionCode);
+ }
+
+ ///
+ public IssueIdentityTokenRequest ToIssueTokenRequest(IssueTokenApiRequest request)
+ {
+ return new IssueIdentityTokenRequest(request.SubjectId, request.TenantId);
+ }
+
+ ///
+ public IssueTokenApiResponse ToIssueTokenApiResponse(IssueIdentityTokenResponse response)
+ {
+ return new IssueTokenApiResponse(response.Token, response.ExpiresInSeconds);
+ }
+
+ ///
+ public RefreshIdentitySessionRequest ToRefreshSessionRequest(RefreshSessionApiRequest request)
+ {
+ return new RefreshIdentitySessionRequest(request.RefreshToken, request.CorrelationId);
+ }
+
+ ///
+ public RefreshSessionApiResponse ToRefreshSessionApiResponse(RefreshIdentitySessionResponse response)
+ {
+ return new RefreshSessionApiResponse(response.Token, response.ExpiresInSeconds);
+ }
+}
diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs
new file mode 100644
index 0000000..c06c1ee
--- /dev/null
+++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs
@@ -0,0 +1,22 @@
+using Thalos.Bff.Application.Grpc;
+using Thalos.Bff.Contracts.Api;
+
+namespace Thalos.Bff.Application.Adapters;
+
+///
+/// Default adapter implementation for identity edge gRPC contract translation.
+///
+public sealed class IdentityEdgeGrpcContractAdapter : IIdentityEdgeGrpcContractAdapter
+{
+ ///
+ public IssueIdentityTokenGrpcContract ToGrpc(IssueTokenApiRequest request)
+ {
+ return new IssueIdentityTokenGrpcContract(request.SubjectId, request.TenantId, request.CorrelationId);
+ }
+
+ ///
+ public IssueTokenApiRequest FromGrpc(IssueIdentityTokenGrpcContract contract)
+ {
+ return new IssueTokenApiRequest(contract.SubjectId, contract.TenantId, contract.CorrelationId);
+ }
+}
diff --git a/src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs b/src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs
new file mode 100644
index 0000000..49f8db4
--- /dev/null
+++ b/src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs
@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Thalos.Bff.Application.Adapters;
+using Thalos.Bff.Application.Handlers;
+using Thalos.Bff.Application.Security;
+
+namespace Thalos.Bff.Application.DependencyInjection;
+
+///
+/// Registers application-layer runtime wiring for thalos-bff.
+///
+public static class ThalosBffApplicationServiceCollectionExtensions
+{
+ ///
+ /// Adds thalos-bff application handlers and adapter implementations.
+ ///
+ /// Service collection.
+ /// Service collection for fluent chaining.
+ public static IServiceCollection AddThalosBffApplicationRuntime(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+
+ services.TryAddScoped();
+ services.TryAddScoped();
+
+ return services;
+ }
+}
diff --git a/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs b/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs
new file mode 100644
index 0000000..964e64c
--- /dev/null
+++ b/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs
@@ -0,0 +1,15 @@
+using Thalos.Service.Identity.Abstractions.Contracts;
+
+namespace Thalos.Bff.Application.Security;
+
+///
+/// Default permission guard backed by thalos-service policy evaluation responses.
+///
+public sealed class IdentityPermissionGuard : IPermissionGuard
+{
+ ///
+ public bool CanAccess(EvaluateIdentityPolicyResponse policyResponse)
+ {
+ return policyResponse.IsAllowed;
+ }
+}
diff --git a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj
index 8edcb4b..206b1cc 100644
--- a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj
+++ b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj
@@ -5,6 +5,7 @@
enable
+
diff --git a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs
new file mode 100644
index 0000000..462f524
--- /dev/null
+++ b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs
@@ -0,0 +1,107 @@
+using Grpc.Core;
+using Microsoft.Extensions.Primitives;
+using Thalos.Bff.Application.Adapters;
+using Thalos.Bff.Application.Contracts;
+using Thalos.Service.Grpc;
+using Thalos.Service.Identity.Abstractions.Contracts;
+
+namespace Thalos.Bff.Rest.Adapters;
+
+///
+/// gRPC-backed adapter for downstream thalos-service calls.
+///
+public sealed class ThalosServiceGrpcClientAdapter(
+ IdentityRuntime.IdentityRuntimeClient grpcClient,
+ IHttpContextAccessor httpContextAccessor,
+ IConfiguration configuration) : IThalosServiceClient
+{
+ private const string CorrelationHeaderName = "x-correlation-id";
+ private readonly string refreshTenantId = configuration["ThalosService:RefreshTenantId"] ?? "refresh";
+
+ ///
+ public async Task IssueTokenAsync(IssueIdentityTokenRequest request)
+ {
+ var correlationId = ResolveCorrelationId();
+ var grpcRequest = new IssueIdentityTokenGrpcRequest
+ {
+ SubjectId = request.SubjectId,
+ TenantId = request.TenantId
+ };
+
+ var grpcResponse = await grpcClient.IssueIdentityTokenAsync(
+ grpcRequest,
+ headers: CreateHeaders(correlationId));
+
+ return new IssueIdentityTokenResponse(grpcResponse.Token, grpcResponse.ExpiresInSeconds);
+ }
+
+ ///
+ public async Task EvaluatePolicyAsync(EvaluateIdentityPolicyRequest request)
+ {
+ var correlationId = ResolveCorrelationId();
+ var grpcRequest = new EvaluateIdentityPolicyGrpcRequest
+ {
+ SubjectId = request.SubjectId,
+ TenantId = request.TenantId,
+ PermissionCode = request.PermissionCode
+ };
+
+ var grpcResponse = await grpcClient.EvaluateIdentityPolicyAsync(
+ grpcRequest,
+ headers: CreateHeaders(correlationId));
+
+ return new EvaluateIdentityPolicyResponse(
+ grpcResponse.SubjectId,
+ grpcResponse.PermissionCode,
+ grpcResponse.IsAllowed);
+ }
+
+ ///
+ public async Task RefreshSessionAsync(RefreshIdentitySessionRequest request)
+ {
+ var correlationId = ResolveCorrelationId(request.CorrelationId);
+ var grpcRequest = new IssueIdentityTokenGrpcRequest
+ {
+ SubjectId = request.RefreshToken,
+ TenantId = refreshTenantId
+ };
+
+ var grpcResponse = await grpcClient.IssueIdentityTokenAsync(
+ grpcRequest,
+ headers: CreateHeaders(correlationId));
+
+ return new RefreshIdentitySessionResponse(grpcResponse.Token, grpcResponse.ExpiresInSeconds);
+ }
+
+ private string ResolveCorrelationId(string? preferred = null)
+ {
+ if (!string.IsNullOrWhiteSpace(preferred))
+ {
+ return preferred;
+ }
+
+ var context = httpContextAccessor.HttpContext;
+ if (context?.Items.TryGetValue(CorrelationHeaderName, out var itemValue) == true &&
+ itemValue is string itemCorrelationId &&
+ !string.IsNullOrWhiteSpace(itemCorrelationId))
+ {
+ return itemCorrelationId;
+ }
+
+ if (context?.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) == true &&
+ !StringValues.IsNullOrEmpty(headerValue))
+ {
+ return headerValue.ToString();
+ }
+
+ return context?.TraceIdentifier ?? $"corr-{Guid.NewGuid():N}";
+ }
+
+ private static Metadata CreateHeaders(string correlationId)
+ {
+ return new Metadata
+ {
+ { CorrelationHeaderName, correlationId }
+ };
+ }
+}
diff --git a/src/Thalos.Bff.Rest/Program.cs b/src/Thalos.Bff.Rest/Program.cs
index df15eb4..3944eee 100644
--- a/src/Thalos.Bff.Rest/Program.cs
+++ b/src/Thalos.Bff.Rest/Program.cs
@@ -1,18 +1,93 @@
+using Core.Blueprint.Common.DependencyInjection;
+using Microsoft.Extensions.Primitives;
+using Thalos.Bff.Application.Adapters;
+using Thalos.Bff.Application.DependencyInjection;
+using Thalos.Bff.Application.Handlers;
using Thalos.Bff.Contracts.Api;
+using Thalos.Bff.Rest.Adapters;
+using Thalos.Bff.Rest.Endpoints;
+using Thalos.Service.Grpc;
+
+const string CorrelationHeaderName = "x-correlation-id";
var builder = WebApplication.CreateBuilder(args);
-// Stage 3 skeleton: single active external protocol for this deployment is REST.
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddHealthChecks();
+builder.Services.AddBlueprintRuntimeCore();
+builder.Services.AddThalosBffApplicationRuntime();
+builder.Services.AddScoped();
+builder.Services.AddGrpcClient(options =>
+{
+ var serviceAddress = builder.Configuration["ThalosService:GrpcAddress"] ?? "http://localhost:5251";
+ options.Address = new Uri(serviceAddress);
+});
+
var app = builder.Build();
-app.MapPost("/api/identity/token", (IssueTokenApiRequest request) =>
+app.Use(async (context, next) =>
{
- return Results.Ok(new IssueTokenApiResponse("", 0));
+ var correlationId = ResolveCorrelationId(context);
+ context.Items[CorrelationHeaderName] = correlationId;
+ context.Request.Headers[CorrelationHeaderName] = correlationId;
+ context.Response.Headers[CorrelationHeaderName] = correlationId;
+ await next();
});
-app.MapPost("/api/identity/session/refresh", (RefreshSessionApiRequest request) =>
+app.MapPost($"{EndpointConventions.ApiPrefix}/token", async (
+ IssueTokenApiRequest request,
+ HttpContext context,
+ IIssueTokenHandler handler) =>
{
- return Results.Ok(new RefreshSessionApiResponse("", 0));
+ var normalizedRequest = request with { CorrelationId = ResolveCorrelationId(context, request.CorrelationId) };
+
+ try
+ {
+ var response = await handler.HandleAsync(normalizedRequest);
+ return Results.Ok(response);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return Results.Unauthorized();
+ }
});
+app.MapPost($"{EndpointConventions.ApiPrefix}/session/refresh", async (
+ RefreshSessionApiRequest request,
+ HttpContext context,
+ IRefreshSessionHandler handler) =>
+{
+ var normalizedRequest = request with { CorrelationId = ResolveCorrelationId(context, request.CorrelationId) };
+ var response = await handler.HandleAsync(normalizedRequest);
+ return Results.Ok(response);
+});
+
+app.MapHealthChecks("/healthz");
+
app.Run();
+
+string ResolveCorrelationId(HttpContext context, string? preferred = null)
+{
+ if (!string.IsNullOrWhiteSpace(preferred))
+ {
+ context.Items[CorrelationHeaderName] = preferred;
+ context.Request.Headers[CorrelationHeaderName] = preferred;
+ context.Response.Headers[CorrelationHeaderName] = preferred;
+ return preferred;
+ }
+
+ if (context.Items.TryGetValue(CorrelationHeaderName, out var itemValue) &&
+ itemValue is string itemCorrelationId &&
+ !string.IsNullOrWhiteSpace(itemCorrelationId))
+ {
+ return itemCorrelationId;
+ }
+
+ if (context.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) &&
+ !StringValues.IsNullOrEmpty(headerValue))
+ {
+ return headerValue.ToString();
+ }
+
+ return context.TraceIdentifier;
+}
diff --git a/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj b/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj
index 256722a..5188257 100644
--- a/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj
+++ b/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj
@@ -4,8 +4,22 @@
enable
enable
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+