diff --git a/.gitignore b/.gitignore
index 31c7257..89d521f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,8 @@
.tasks/
.agile/
+bin/
+obj/
+TestResults/
+.vs/
+*.user
+*.suo
diff --git a/Thalos.Domain.slnx b/Thalos.Domain.slnx
new file mode 100644
index 0000000..dc4bd19
--- /dev/null
+++ b/Thalos.Domain.slnx
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/docs/migration/policy-behavior-invariants.md b/docs/migration/policy-behavior-invariants.md
index 8c84ef2..1e50ace 100644
--- a/docs/migration/policy-behavior-invariants.md
+++ b/docs/migration/policy-behavior-invariants.md
@@ -3,6 +3,9 @@
## Invariants
- Equivalent policy inputs produce equivalent policy decisions.
- Token decision fallback behavior remains stable until explicitly revised.
+- Provider semantics are explicit:
+ - `InternalJwt`: standard identity permission evaluation.
+ - `AzureAd` and `Google`: policy permission must remain within `identity.*` scope.
- Service transport contracts remain stable during domain extraction.
## Validation Approach
diff --git a/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs b/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
new file mode 100644
index 0000000..b40cf45
--- /dev/null
+++ b/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
@@ -0,0 +1,18 @@
+using BuildingBlock.Identity.Contracts.Conventions;
+
+namespace Thalos.Domain.Contracts;
+
+///
+/// Domain input describing policy context and granted permissions for evaluation.
+///
+/// Identity subject identifier.
+/// Permission code being evaluated.
+/// Auth provider used in the request flow.
+/// Whether contextual constraints are satisfied.
+/// Permissions granted to subject in tenant scope.
+public sealed record IdentityPolicyContextData(
+ string SubjectId,
+ string PermissionCode,
+ IdentityAuthProvider Provider,
+ bool ContextSatisfied,
+ IReadOnlyCollection GrantedPermissions);
diff --git a/src/Thalos.Domain/Contracts/IdentityTokenData.cs b/src/Thalos.Domain/Contracts/IdentityTokenData.cs
new file mode 100644
index 0000000..f43d312
--- /dev/null
+++ b/src/Thalos.Domain/Contracts/IdentityTokenData.cs
@@ -0,0 +1,14 @@
+using BuildingBlock.Identity.Contracts.Conventions;
+
+namespace Thalos.Domain.Contracts;
+
+///
+/// Domain input describing issued token projection from technical persistence.
+///
+/// Token value, if found.
+/// Token lifetime, if found.
+/// Auth provider used in the issuance flow.
+public sealed record IdentityTokenData(
+ string? Token,
+ int? ExpiresInSeconds,
+ IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt);
diff --git a/src/Thalos.Domain/Conventions/ThalosDomainPackageContract.cs b/src/Thalos.Domain/Conventions/ThalosDomainPackageContract.cs
new file mode 100644
index 0000000..961458b
--- /dev/null
+++ b/src/Thalos.Domain/Conventions/ThalosDomainPackageContract.cs
@@ -0,0 +1,8 @@
+namespace Thalos.Domain.Conventions;
+
+///
+/// Marker type for thalos-domain package discovery.
+///
+public sealed class ThalosDomainPackageContract
+{
+}
diff --git a/src/Thalos.Domain/Decisions/IIdentityPolicyDecisionService.cs b/src/Thalos.Domain/Decisions/IIdentityPolicyDecisionService.cs
new file mode 100644
index 0000000..44c17ac
--- /dev/null
+++ b/src/Thalos.Domain/Decisions/IIdentityPolicyDecisionService.cs
@@ -0,0 +1,23 @@
+using BuildingBlock.Identity.Contracts.Requests;
+using BuildingBlock.Identity.Contracts.Responses;
+using Thalos.Domain.Contracts;
+
+namespace Thalos.Domain.Decisions;
+
+///
+/// Defines domain decision boundary for identity policy workflows.
+///
+public interface IIdentityPolicyDecisionService
+{
+ ///
+ /// Builds policy context request from policy evaluation request.
+ ///
+ IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request);
+
+ ///
+ /// Evaluates policy response from contextual data.
+ ///
+ EvaluateIdentityPolicyResponse Evaluate(
+ EvaluateIdentityPolicyRequest request,
+ IdentityPolicyContextData policyContextData);
+}
diff --git a/src/Thalos.Domain/Decisions/IIdentityTokenDecisionService.cs b/src/Thalos.Domain/Decisions/IIdentityTokenDecisionService.cs
new file mode 100644
index 0000000..0f161ea
--- /dev/null
+++ b/src/Thalos.Domain/Decisions/IIdentityTokenDecisionService.cs
@@ -0,0 +1,15 @@
+using BuildingBlock.Identity.Contracts.Responses;
+using Thalos.Domain.Contracts;
+
+namespace Thalos.Domain.Decisions;
+
+///
+/// Defines domain decision boundary for identity token issuance semantics.
+///
+public interface IIdentityTokenDecisionService
+{
+ ///
+ /// Builds token response from technical token data using domain fallback policy.
+ ///
+ IssueIdentityTokenResponse BuildIssuedTokenResponse(IdentityTokenData tokenData);
+}
diff --git a/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs b/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
new file mode 100644
index 0000000..c382ff6
--- /dev/null
+++ b/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
@@ -0,0 +1,59 @@
+using BuildingBlock.Identity.Contracts.Requests;
+using BuildingBlock.Identity.Contracts.Responses;
+using BuildingBlock.Identity.Contracts.Conventions;
+using Thalos.Domain.Contracts;
+
+namespace Thalos.Domain.Decisions;
+
+///
+/// Default domain implementation for identity policy decision workflows.
+///
+public sealed class IdentityPolicyDecisionService : IIdentityPolicyDecisionService
+{
+ ///
+ public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request)
+ {
+ return new IdentityPolicyContextRequest(
+ request.SubjectId,
+ request.TenantId,
+ request.PermissionCode,
+ request.Provider);
+ }
+
+ ///
+ public EvaluateIdentityPolicyResponse Evaluate(
+ EvaluateIdentityPolicyRequest request,
+ IdentityPolicyContextData policyContextData)
+ {
+ var permissionMatched = policyContextData.GrantedPermissions.Any(permission =>
+ string.Equals(permission, request.PermissionCode, StringComparison.OrdinalIgnoreCase));
+ var providerSatisfied = IsProviderContextSatisfied(request.Provider, policyContextData);
+
+ return new EvaluateIdentityPolicyResponse(
+ request.SubjectId,
+ request.PermissionCode,
+ providerSatisfied && permissionMatched);
+ }
+
+ private static bool IsProviderContextSatisfied(
+ IdentityAuthProvider provider,
+ IdentityPolicyContextData policyContextData)
+ {
+ if (!policyContextData.ContextSatisfied)
+ {
+ return false;
+ }
+
+ return provider switch
+ {
+ IdentityAuthProvider.InternalJwt => true,
+ IdentityAuthProvider.AzureAd => policyContextData.PermissionCode.StartsWith(
+ "identity.",
+ StringComparison.OrdinalIgnoreCase),
+ IdentityAuthProvider.Google => policyContextData.PermissionCode.StartsWith(
+ "identity.",
+ StringComparison.OrdinalIgnoreCase),
+ _ => false
+ };
+ }
+}
diff --git a/src/Thalos.Domain/Decisions/IdentityTokenDecisionService.cs b/src/Thalos.Domain/Decisions/IdentityTokenDecisionService.cs
new file mode 100644
index 0000000..22fbb9e
--- /dev/null
+++ b/src/Thalos.Domain/Decisions/IdentityTokenDecisionService.cs
@@ -0,0 +1,21 @@
+using BuildingBlock.Identity.Contracts.Responses;
+using Thalos.Domain.Contracts;
+
+namespace Thalos.Domain.Decisions;
+
+///
+/// Default domain implementation for token issuance fallback semantics.
+///
+public sealed class IdentityTokenDecisionService : IIdentityTokenDecisionService
+{
+ ///
+ public IssueIdentityTokenResponse BuildIssuedTokenResponse(IdentityTokenData tokenData)
+ {
+ if (string.IsNullOrWhiteSpace(tokenData.Token) || !tokenData.ExpiresInSeconds.HasValue)
+ {
+ return new IssueIdentityTokenResponse(string.Empty, 0);
+ }
+
+ return new IssueIdentityTokenResponse(tokenData.Token, tokenData.ExpiresInSeconds.Value);
+ }
+}
diff --git a/src/Thalos.Domain/Thalos.Domain.csproj b/src/Thalos.Domain/Thalos.Domain.csproj
new file mode 100644
index 0000000..20d5591
--- /dev/null
+++ b/src/Thalos.Domain/Thalos.Domain.csproj
@@ -0,0 +1,10 @@
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
diff --git a/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs b/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs
new file mode 100644
index 0000000..c0bb086
--- /dev/null
+++ b/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs
@@ -0,0 +1,72 @@
+using BuildingBlock.Identity.Contracts.Requests;
+using BuildingBlock.Identity.Contracts.Conventions;
+using Thalos.Domain.Contracts;
+using Thalos.Domain.Decisions;
+
+namespace Thalos.Domain.UnitTests;
+
+public class IdentityPolicyDecisionServiceTests
+{
+ [Fact]
+ public void Evaluate_WhenPermissionMatchedAndContextSatisfied_ReturnsAllowed()
+ {
+ var service = new IdentityPolicyDecisionService();
+ var request = new EvaluateIdentityPolicyRequest(
+ "user-1",
+ "tenant-1",
+ "identity.token.issue",
+ IdentityAuthProvider.InternalJwt);
+ var context = new IdentityPolicyContextData(
+ request.SubjectId,
+ request.PermissionCode,
+ request.Provider,
+ true,
+ ["identity.token.issue", "identity.policy.evaluate"]);
+
+ var response = service.Evaluate(request, context);
+
+ Assert.True(response.IsAllowed);
+ }
+
+ [Fact]
+ public void Evaluate_WhenPermissionMissing_ReturnsDenied()
+ {
+ var service = new IdentityPolicyDecisionService();
+ var request = new EvaluateIdentityPolicyRequest(
+ "user-1",
+ "tenant-1",
+ "identity.token.issue",
+ IdentityAuthProvider.InternalJwt);
+ var context = new IdentityPolicyContextData(
+ request.SubjectId,
+ request.PermissionCode,
+ request.Provider,
+ true,
+ ["identity.read"]);
+
+ var response = service.Evaluate(request, context);
+
+ Assert.False(response.IsAllowed);
+ }
+
+ [Fact]
+ public void Evaluate_WhenProviderIsExternalAndPermissionPrefixInvalid_ReturnsDenied()
+ {
+ var service = new IdentityPolicyDecisionService();
+ var request = new EvaluateIdentityPolicyRequest(
+ "user-2",
+ "tenant-2",
+ "catalog.read",
+ IdentityAuthProvider.AzureAd);
+ var context = new IdentityPolicyContextData(
+ request.SubjectId,
+ request.PermissionCode,
+ request.Provider,
+ true,
+ ["catalog.read"]);
+
+ var response = service.Evaluate(request, context);
+
+ Assert.False(response.IsAllowed);
+ }
+}
diff --git a/tests/Thalos.Domain.UnitTests/IdentityTokenDecisionServiceTests.cs b/tests/Thalos.Domain.UnitTests/IdentityTokenDecisionServiceTests.cs
new file mode 100644
index 0000000..9303fe9
--- /dev/null
+++ b/tests/Thalos.Domain.UnitTests/IdentityTokenDecisionServiceTests.cs
@@ -0,0 +1,29 @@
+using Thalos.Domain.Contracts;
+using Thalos.Domain.Decisions;
+
+namespace Thalos.Domain.UnitTests;
+
+public class IdentityTokenDecisionServiceTests
+{
+ [Fact]
+ public void BuildIssuedTokenResponse_WhenTokenMissing_ReturnsFallbackShape()
+ {
+ var service = new IdentityTokenDecisionService();
+
+ var response = service.BuildIssuedTokenResponse(new IdentityTokenData(null, null));
+
+ Assert.Equal(string.Empty, response.Token);
+ Assert.Equal(0, response.ExpiresInSeconds);
+ }
+
+ [Fact]
+ public void BuildIssuedTokenResponse_WhenTokenExists_ReturnsIssuedToken()
+ {
+ var service = new IdentityTokenDecisionService();
+
+ var response = service.BuildIssuedTokenResponse(new IdentityTokenData("token-123", 1800));
+
+ Assert.Equal("token-123", response.Token);
+ Assert.Equal(1800, response.ExpiresInSeconds);
+ }
+}
diff --git a/tests/Thalos.Domain.UnitTests/Thalos.Domain.UnitTests.csproj b/tests/Thalos.Domain.UnitTests/Thalos.Domain.UnitTests.csproj
new file mode 100644
index 0000000..01a6c86
--- /dev/null
+++ b/tests/Thalos.Domain.UnitTests/Thalos.Domain.UnitTests.csproj
@@ -0,0 +1,20 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+