feat(stage3): scaffold task-001 baseline

- WHY: establish Stage 3 task-001 execution baseline per repo intent
- WHAT: add minimal solution/project skeleton and boundary docs
- RULE: apply stage3 execution runtime and repository workflow directives
This commit is contained in:
José René White Enciso 2026-02-22 01:30:02 -06:00
parent eb664ef06e
commit 9ca6b4c110
21 changed files with 321 additions and 0 deletions

10
Thalos.Bff.slnx Normal file
View File

@ -0,0 +1,10 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj" />
<Project Path="src/Thalos.Bff.Application/Thalos.Bff.Application.csproj" />
<Project Path="src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Thalos.Bff.Application.UnitTests/Thalos.Bff.Application.UnitTests.csproj" />
</Folder>
</Solution>

View File

@ -0,0 +1,16 @@
# Identity Edge API
## Active External Protocol
- REST is the active external protocol for this BFF deployment.
## Entrypoints
- `POST /api/identity/token`
- `POST /api/identity/session/refresh`
## Boundary Notes
- Endpoint handlers perform edge validation and permission checks.
- Business orchestration remains in thalos-service.
- Identity abstractions remain owned by Thalos repositories.

View File

@ -0,0 +1,27 @@
@startuml
skinparam packageStyle rectangle
package "thalos-bff" {
class Program
interface IIssueTokenHandler
class IssueTokenHandler
interface IRefreshSessionHandler
class RefreshSessionHandler
interface IPermissionGuard
interface IThalosServiceClient
IssueTokenHandler ..|> IIssueTokenHandler
RefreshSessionHandler ..|> IRefreshSessionHandler
IssueTokenHandler --> IPermissionGuard
IssueTokenHandler --> IThalosServiceClient
RefreshSessionHandler --> IThalosServiceClient
}
package "Clients" as Clients
package "thalos-service" as ThalosService
Clients --> Program : REST
Program --> IIssueTokenHandler
Program --> IRefreshSessionHandler
IThalosServiceClient ..> ThalosService : gRPC/internal
@enduml

View File

@ -0,0 +1,11 @@
# Permission Enforcement Map
## Enforcement Points
- `identity.token.issue` evaluated at token issuance handler.
- Session refresh guarded by edge session validation policy.
## Guardrail
- Permission checks happen at BFF entrypoints before downstream calls.
- Authorization decisions are explicit and traceable at edge boundaries.

View File

@ -0,0 +1,23 @@
using Thalos.Bff.Contracts.Api;
namespace Thalos.Bff.Application.Adapters;
/// <summary>
/// Adapter boundary for downstream thalos-service calls.
/// </summary>
public interface IThalosServiceClient
{
/// <summary>
/// Requests token issuance from thalos-service.
/// </summary>
/// <param name="request">Token issuance request.</param>
/// <returns>Token issuance response.</returns>
Task<IssueTokenApiResponse> IssueTokenAsync(IssueTokenApiRequest request);
/// <summary>
/// Requests token refresh from thalos-service.
/// </summary>
/// <param name="request">Session refresh request.</param>
/// <returns>Session refresh response.</returns>
Task<RefreshSessionApiResponse> RefreshSessionAsync(RefreshSessionApiRequest request);
}

View File

@ -0,0 +1,16 @@
using Thalos.Bff.Contracts.Api;
namespace Thalos.Bff.Application.Handlers;
/// <summary>
/// Edge handler boundary for token issuance entrypoint.
/// </summary>
public interface IIssueTokenHandler
{
/// <summary>
/// Handles token issuance flow.
/// </summary>
/// <param name="request">Token issuance request.</param>
/// <returns>Token issuance response.</returns>
Task<IssueTokenApiResponse> HandleAsync(IssueTokenApiRequest request);
}

View File

@ -0,0 +1,16 @@
using Thalos.Bff.Contracts.Api;
namespace Thalos.Bff.Application.Handlers;
/// <summary>
/// Edge handler boundary for session refresh entrypoint.
/// </summary>
public interface IRefreshSessionHandler
{
/// <summary>
/// Handles session refresh flow.
/// </summary>
/// <param name="request">Session refresh request.</param>
/// <returns>Session refresh response.</returns>
Task<RefreshSessionApiResponse> HandleAsync(RefreshSessionApiRequest request);
}

View File

@ -0,0 +1,23 @@
using Thalos.Bff.Application.Adapters;
using Thalos.Bff.Application.Security;
using Thalos.Bff.Contracts.Api;
namespace Thalos.Bff.Application.Handlers;
/// <summary>
/// Default edge handler for token issuance.
/// </summary>
public sealed class IssueTokenHandler(IThalosServiceClient serviceClient, IPermissionGuard permissionGuard)
: IIssueTokenHandler
{
/// <inheritdoc />
public Task<IssueTokenApiResponse> HandleAsync(IssueTokenApiRequest request)
{
if (!permissionGuard.CanAccess("identity.token.issue"))
{
throw new UnauthorizedAccessException("Permission denied.");
}
return serviceClient.IssueTokenAsync(request);
}
}

View File

@ -0,0 +1,17 @@
using Thalos.Bff.Application.Adapters;
using Thalos.Bff.Contracts.Api;
namespace Thalos.Bff.Application.Handlers;
/// <summary>
/// Default edge handler for refresh session flow.
/// </summary>
public sealed class RefreshSessionHandler(IThalosServiceClient serviceClient)
: IRefreshSessionHandler
{
/// <inheritdoc />
public Task<RefreshSessionApiResponse> HandleAsync(RefreshSessionApiRequest request)
{
return serviceClient.RefreshSessionAsync(request);
}
}

View File

@ -0,0 +1,14 @@
namespace Thalos.Bff.Application.Security;
/// <summary>
/// Edge permission enforcement contract.
/// </summary>
public interface IPermissionGuard
{
/// <summary>
/// Evaluates whether a permission is satisfied for the current request context.
/// </summary>
/// <param name="permissionCode">Permission code to evaluate.</param>
/// <returns>True when access is allowed.</returns>
bool CanAccess(string permissionCode);
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Thalos.Bff.Contracts\Thalos.Bff.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace Thalos.Bff.Contracts.Api;
/// <summary>
/// External API request for identity token issuance.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="TenantId">Tenant identifier.</param>
public sealed record IssueTokenApiRequest(string SubjectId, string TenantId);

View File

@ -0,0 +1,8 @@
namespace Thalos.Bff.Contracts.Api;
/// <summary>
/// External API response for identity token issuance.
/// </summary>
/// <param name="AccessToken">Issued access token.</param>
/// <param name="ExpiresInSeconds">Token expiration in seconds.</param>
public sealed record IssueTokenApiResponse(string AccessToken, int ExpiresInSeconds);

View File

@ -0,0 +1,7 @@
namespace Thalos.Bff.Contracts.Api;
/// <summary>
/// External API request for refresh token session flow.
/// </summary>
/// <param name="RefreshToken">Refresh token value.</param>
public sealed record RefreshSessionApiRequest(string RefreshToken);

View File

@ -0,0 +1,8 @@
namespace Thalos.Bff.Contracts.Api;
/// <summary>
/// External API response for refresh token session flow.
/// </summary>
/// <param name="AccessToken">Refreshed access token.</param>
/// <param name="ExpiresInSeconds">New token expiration in seconds.</param>
public sealed record RefreshSessionApiResponse(string AccessToken, int ExpiresInSeconds);

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,12 @@
namespace Thalos.Bff.Rest.Endpoints;
/// <summary>
/// Defines endpoint conventions for the identity edge API.
/// </summary>
public static class EndpointConventions
{
/// <summary>
/// Prefix used by identity edge API routes.
/// </summary>
public const string ApiPrefix = "/api/identity";
}

View File

@ -0,0 +1,18 @@
using Thalos.Bff.Contracts.Api;
var builder = WebApplication.CreateBuilder(args);
// Stage 3 skeleton: single active external protocol for this deployment is REST.
var app = builder.Build();
app.MapPost("/api/identity/token", (IssueTokenApiRequest request) =>
{
return Results.Ok(new IssueTokenApiResponse("", 0));
});
app.MapPost("/api/identity/session/refresh", (RefreshSessionApiRequest request) =>
{
return Results.Ok(new RefreshSessionApiResponse("", 0));
});
app.Run();

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Thalos.Bff.Application\Thalos.Bff.Application.csproj" />
<ProjectReference Include="..\Thalos.Bff.Contracts\Thalos.Bff.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,38 @@
using Thalos.Bff.Application.Adapters;
using Thalos.Bff.Application.Handlers;
using Thalos.Bff.Application.Security;
using Thalos.Bff.Contracts.Api;
namespace Thalos.Bff.Application.UnitTests;
public class IssueTokenHandlerTests
{
[Fact]
public async Task HandleAsync_WhenPermissionAllowed_DelegatesToServiceClient()
{
var handler = new IssueTokenHandler(new FakeThalosServiceClient(), new AllowPermissionGuard());
var response = await handler.HandleAsync(new IssueTokenApiRequest("user-1", "tenant-1"));
Assert.Equal("token-xyz", response.AccessToken);
Assert.Equal(1800, response.ExpiresInSeconds);
}
private sealed class FakeThalosServiceClient : IThalosServiceClient
{
public Task<IssueTokenApiResponse> IssueTokenAsync(IssueTokenApiRequest request)
{
return Task.FromResult(new IssueTokenApiResponse("token-xyz", 1800));
}
public Task<RefreshSessionApiResponse> RefreshSessionAsync(RefreshSessionApiRequest request)
{
return Task.FromResult(new RefreshSessionApiResponse("token-refreshed", 1800));
}
}
private sealed class AllowPermissionGuard : IPermissionGuard
{
public bool CanAccess(string permissionCode) => true;
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Thalos.Bff.Application\Thalos.Bff.Application.csproj" />
<ProjectReference Include="..\..\src\Thalos.Bff.Contracts\Thalos.Bff.Contracts.csproj" />
</ItemGroup>
</Project>