Add determinism tests for verdict artifact generation and update SHA256 sums script

- Implemented comprehensive tests for verdict artifact generation to ensure deterministic outputs across various scenarios, including identical inputs, parallel execution, and change ordering.
- Created helper methods for generating sample verdict inputs and computing canonical hashes.
- Added tests to validate the stability of canonical hashes, proof spine ordering, and summary statistics.
- Introduced a new PowerShell script to update SHA256 sums for files, ensuring accurate hash generation and file integrity checks.
This commit is contained in:
StellaOps Bot
2025-12-24 02:17:34 +02:00
parent e59921374e
commit 7503c19b8f
390 changed files with 37389 additions and 5380 deletions

View File

@@ -56,6 +56,7 @@ The Router replaces the Serdica HTTP-to-RabbitMQ pattern with a simpler, generic
| TLS Transport | `StellaOps.Router.Transport.Tls` | Encrypted TCP transport |
| UDP Transport | `StellaOps.Router.Transport.Udp` | Small payload transport |
| RabbitMQ Transport | `StellaOps.Router.Transport.RabbitMQ` | Message queue transport |
| Messaging Transport | `StellaOps.Router.Transport.Messaging` | Messaging/RPC transport (Valkey-backed via `StellaOps.Messaging.Transport.Valkey`) |
## Solution Structure
@@ -86,6 +87,8 @@ StellaOps.Router.slnx
| [openapi-aggregation.md](openapi-aggregation.md) | OpenAPI document generation |
| [migration-guide.md](migration-guide.md) | WebService to Microservice migration |
| [rate-limiting.md](rate-limiting.md) | Centralized router rate limiting |
| [aspnet-endpoint-bridge.md](aspnet-endpoint-bridge.md) | Using ASP.NET endpoint registration as Router endpoint registration |
| [messaging-valkey-transport.md](messaging-valkey-transport.md) | Messaging transport over Valkey |
## Quick Start

View File

@@ -0,0 +1,857 @@
# Router · ASP.NET Endpoint Bridge
## Status
- **Version:** 0.2 (revised)
- **Last updated:** 2025-12-24 (UTC)
- **Implementation sprint:** `SPRINT_8100_0011_0001`
---
## Problem Statement
StellaOps Router routes external HTTP requests (via Gateway) to internal microservices over binary transports (TCP/TLS/Messaging). Most StellaOps microservices are ASP.NET Core WebServices that register routes via standard ASP.NET endpoint registration (controllers or minimal APIs).
**Current behavior:** `StellaOps.Microservice` discovers and registers only Router-native endpoints implemented via `[StellaEndpoint]` handlers. If a service has only ASP.NET endpoints, it either:
- Registers **zero** Router endpoints (Gateway cannot route to it), or
- Duplicates routes as `[StellaEndpoint]` handlers (route drift + duplicated security metadata).
**Desired behavior:** Treat ASP.NET endpoint registration as the **single source of truth** and automatically bridge it to Router endpoint registration.
---
## Design Goals
| Goal | Description |
|------|-------------|
| **Single source of truth** | ASP.NET endpoint registration defines what the service exposes |
| **Full ASP.NET fidelity** | Authorization, filters, model binding, and all ASP.NET features work correctly |
| **Determinism** | Endpoint discovery produces stable ordering and normalized paths |
| **Security alignment** | Authorization metadata flows to Router `RequiringClaims` |
| **Opt-in integration** | Services explicitly enable the bridge via `Program.cs` |
| **Offline-first** | No runtime network dependency for discovery/dispatch |
---
## Non-Goals (v0.1)
| Feature | Reason |
|---------|--------|
| SignalR/WebSocket support | Different protocol semantics |
| gRPC endpoint bridging | Different protocol |
| Streaming request bodies | Router SDK buffering limitation |
| Custom route constraints | Complexity; document as limitation |
| API versioning (header/query) | Complexity; use path-based versioning |
| Automatic schema generation | Depends on source generators; future enhancement |
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ ASP.NET WebService │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Program.cs │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ builder.Services.AddStellaRouterBridge(options => { │ │ │
│ │ │ options.ServiceName = "scanner"; │ │ │
│ │ │ options.Version = "1.0.0"; │ │ │
│ │ │ }); │ │ │
│ │ │ app.UseStellaRouterBridge(); │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ StellaOps.Microservice.AspNetCore │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ Discovery Provider │ │ Request Dispatcher │ │ │
│ │ │ ─────────────────── │ │ ─────────────────────────────── │ │ │
│ │ │ • EndpointDataSource│ │ • RequestFrame → HttpContext │ │ │
│ │ │ • Metadata extraction│ │ • ASP.NET pipeline execution │ │ │
│ │ │ • Route normalization│ │ • ResponseFrame capture │ │ │
│ │ │ • Auth claim mapping │ │ • DI scope management │ │ │
│ │ └──────────┬──────────┘ └──────────────┬──────────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Router SDK (StellaOps.Microservice) │ │ │
│ │ │ • HELLO registration with discovered endpoints │ │ │
│ │ │ • Request routing to dispatcher │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ASP.NET Endpoints (unchanged) │ │
│ │ • Minimal APIs: app.MapGet("/api/...", handler) │ │
│ │ • Controllers: [ApiController] with [HttpGet], etc. │ │
│ │ • Route groups: app.MapGroup("/api").MapScannerEndpoints() │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ Router Transport (TCP/TLS/Messaging)
┌───────────────────────────────┐
│ StellaOps.Gateway.WebService │
│ • HELLO processing │
│ • Endpoint routing │
│ • OpenAPI aggregation │
└───────────────────────────────┘
```
---
## Component Design
### 1. StellaRouterBridgeOptions
Configuration for the bridge, specified in `Program.cs`:
```csharp
public sealed class StellaRouterBridgeOptions
{
// === Required: Service Identity ===
public required string ServiceName { get; set; }
public required string Version { get; set; }
public required string Region { get; set; }
public string? InstanceId { get; set; } // Auto-generated if null
// === Authorization Mapping ===
public AuthorizationMappingStrategy AuthorizationMapping { get; set; }
= AuthorizationMappingStrategy.Hybrid;
public MissingAuthorizationBehavior OnMissingAuthorization { get; set; }
= MissingAuthorizationBehavior.RequireExplicit;
// === YAML Overrides ===
public string? YamlConfigPath { get; set; }
// === Metadata Extraction ===
public bool ExtractSchemas { get; set; } = true;
public bool ExtractOpenApiMetadata { get; set; } = true;
// === Route Handling ===
public UnsupportedConstraintBehavior OnUnsupportedConstraint { get; set; }
= UnsupportedConstraintBehavior.WarnAndStrip;
public Func<RouteEndpoint, bool>? EndpointFilter { get; set; }
// === Defaults ===
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(30);
}
public enum AuthorizationMappingStrategy
{
YamlOnly, // Only use YAML overrides
AspNetMetadataOnly, // Only use ASP.NET metadata
Hybrid // ASP.NET + YAML (YAML wins on conflict)
}
public enum MissingAuthorizationBehavior
{
RequireExplicit, // Fail if no auth metadata
AllowAuthenticated, // Allow with empty claims (authenticated required)
WarnAndAllow // Log warning, allow with empty claims
}
public enum UnsupportedConstraintBehavior
{
Fail, // Fail discovery on unsupported constraint
WarnAndStrip, // Log warning, strip constraint
SilentStrip // Strip without warning
}
```
### 2. AspNetEndpointDescriptor
Extended endpoint descriptor with full ASP.NET metadata:
```csharp
public sealed record AspNetEndpointDescriptor
{
// === Core Identity ===
public required string ServiceName { get; init; }
public required string Version { get; init; }
public required string Method { get; init; }
public required string Path { get; init; }
public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromSeconds(30);
public bool SupportsStreaming { get; init; }
// === Authorization ===
public IReadOnlyList<ClaimRequirement> RequiringClaims { get; init; } = [];
public IReadOnlyList<string> AuthorizationPolicies { get; init; } = [];
public IReadOnlyList<string> Roles { get; init; } = [];
public bool AllowAnonymous { get; init; }
public AuthorizationSource AuthorizationSource { get; init; }
// === Parameters ===
public IReadOnlyList<ParameterDescriptor> Parameters { get; init; } = [];
// === Responses ===
public IReadOnlyList<ResponseDescriptor> Responses { get; init; } = [];
// === OpenAPI ===
public string? OperationId { get; init; }
public string? Summary { get; init; }
public string? Description { get; init; }
public IReadOnlyList<string> Tags { get; init; } = [];
// === Schema ===
public EndpointSchemaInfo? SchemaInfo { get; init; }
// === Internal ===
internal RouteEndpoint? OriginalEndpoint { get; init; }
internal string? OriginalRoutePattern { get; init; }
/// <summary>
/// Convert to standard EndpointDescriptor for HELLO payload.
/// </summary>
public EndpointDescriptor ToEndpointDescriptor() => new()
{
ServiceName = ServiceName,
Version = Version,
Method = Method,
Path = Path,
DefaultTimeout = DefaultTimeout,
SupportsStreaming = SupportsStreaming,
RequiringClaims = RequiringClaims,
SchemaInfo = SchemaInfo
};
}
public sealed record ParameterDescriptor
{
public required string Name { get; init; }
public required ParameterSource Source { get; init; }
public required Type Type { get; init; }
public bool IsRequired { get; init; } = true;
public object? DefaultValue { get; init; }
public string? Description { get; init; }
}
public enum ParameterSource { Route, Query, Header, Body, Services }
public sealed record ResponseDescriptor
{
public required int StatusCode { get; init; }
public Type? ResponseType { get; init; }
public string? Description { get; init; }
public string? ContentType { get; init; } = "application/json";
}
public enum AuthorizationSource { None, AspNetMetadata, YamlOverride, Hybrid }
```
### 3. IAspNetEndpointDiscoveryProvider
Discovery provider interface:
```csharp
public interface IAspNetEndpointDiscoveryProvider : IEndpointDiscoveryProvider
{
/// <summary>
/// Discover ASP.NET endpoints with full metadata.
/// </summary>
IReadOnlyList<AspNetEndpointDescriptor> DiscoverAspNetEndpoints();
}
```
### 4. IAuthorizationClaimMapper
Authorization-to-claims mapping interface:
```csharp
public interface IAuthorizationClaimMapper
{
/// <summary>
/// Map ASP.NET authorization metadata to Router claim requirements.
/// </summary>
Task<AuthorizationMappingResult> MapAsync(
RouteEndpoint endpoint,
CancellationToken cancellationToken = default);
}
public sealed record AuthorizationMappingResult
{
public IReadOnlyList<ClaimRequirement> Claims { get; init; } = [];
public IReadOnlyList<string> Policies { get; init; } = [];
public IReadOnlyList<string> Roles { get; init; } = [];
public bool AllowAnonymous { get; init; }
public AuthorizationSource Source { get; init; }
}
```
### 5. IAspNetRouterRequestDispatcher
Request dispatch interface:
```csharp
public interface IAspNetRouterRequestDispatcher
{
/// <summary>
/// Dispatch a Router request frame through ASP.NET pipeline.
/// </summary>
Task<ResponseFrame> DispatchAsync(
RequestFrame request,
CancellationToken cancellationToken = default);
}
```
---
## Endpoint Discovery Algorithm
### Step 1: Enumerate Endpoints
```csharp
var endpoints = endpointDataSource.Endpoints
.OfType<RouteEndpoint>()
.Where(e => e.Metadata.GetMetadata<HttpMethodMetadata>() is not null)
.Where(options.EndpointFilter ?? (_ => true));
```
### Step 2: Extract Metadata per Endpoint
For each `RouteEndpoint`:
1. **HTTP Method**: From `HttpMethodMetadata`
2. **Path**: Normalize route pattern (see below)
3. **Authorization**: From `IAuthorizeData`, `IAllowAnonymous`
4. **Parameters**: From route pattern + parameter binding metadata
5. **Responses**: From `IProducesResponseTypeMetadata`
6. **OpenAPI**: From `IEndpointNameMetadata`, `IEndpointSummaryMetadata`, `ITagsMetadata`
### Step 3: Normalize Route Pattern
```csharp
public static string NormalizeRoutePattern(RoutePattern pattern)
{
var raw = pattern.RawText ?? BuildFromSegments(pattern);
// 1. Ensure leading slash
if (!raw.StartsWith('/'))
raw = "/" + raw;
// 2. Strip constraints: {id:int} → {id}
raw = Regex.Replace(raw, @"\{(\*?)([A-Za-z0-9_]+)(:[^}]+)?\}", "{$2}");
// 3. Normalize catch-all: {**path} → {path}
raw = raw.Replace("**", "", StringComparison.Ordinal);
// 4. Remove trailing slash
raw = raw.TrimEnd('/');
// 5. Empty path becomes "/"
return string.IsNullOrEmpty(raw) ? "/" : raw;
}
```
### Step 4: Deterministic Ordering
Sort endpoints for stable HELLO payloads:
```csharp
var ordered = endpoints
.OrderBy(e => e.Path, StringComparer.OrdinalIgnoreCase)
.ThenBy(e => GetMethodOrder(e.Method))
.ThenBy(e => e.OriginalEndpoint?.DisplayName ?? "");
static int GetMethodOrder(string method) => method.ToUpperInvariant() switch
{
"GET" => 0,
"POST" => 1,
"PUT" => 2,
"PATCH" => 3,
"DELETE" => 4,
"OPTIONS" => 5,
"HEAD" => 6,
_ => 7
};
```
---
## Authorization Mapping
### Mapping Rules
| ASP.NET Metadata | Router Mapping |
|------------------|----------------|
| `[Authorize]` (no args) | Empty `RequiringClaims` (authenticated required) |
| `[Authorize(Policy = "X")]` | Resolve policy → claims via `IAuthorizationPolicyProvider` |
| `[Authorize(Roles = "A,B")]` | `ClaimRequirement(ClaimTypes.Role, "A")`, `ClaimRequirement(ClaimTypes.Role, "B")` |
| `[AllowAnonymous]` | `AllowAnonymous = true`, empty claims |
| `.RequireAuthorization("Policy")` | Same as `[Authorize(Policy)]` |
### Policy Resolution
```csharp
public async Task<IReadOnlyList<ClaimRequirement>> ResolvePolicyAsync(
string policyName,
IAuthorizationPolicyProvider policyProvider)
{
var policy = await policyProvider.GetPolicyAsync(policyName);
if (policy is null)
return [];
var claims = new List<ClaimRequirement>();
foreach (var requirement in policy.Requirements)
{
switch (requirement)
{
case ClaimsAuthorizationRequirement claimsReq:
foreach (var value in claimsReq.AllowedValues ?? [null])
{
claims.Add(new ClaimRequirement
{
Type = claimsReq.ClaimType,
Value = value
});
}
break;
case RolesAuthorizationRequirement rolesReq:
foreach (var role in rolesReq.AllowedRoles)
{
claims.Add(new ClaimRequirement
{
Type = ClaimTypes.Role,
Value = role
});
}
break;
// Other requirement types: log warning, continue
}
}
return claims;
}
```
### YAML Override Merge
When `AuthorizationMappingStrategy.Hybrid`:
```csharp
public IReadOnlyList<ClaimRequirement> MergeWithYaml(
IReadOnlyList<ClaimRequirement> aspNetClaims,
EndpointOverrideConfig? yamlOverride)
{
if (yamlOverride?.RequiringClaims is not { Count: > 0 } yamlClaims)
return aspNetClaims;
// YAML completely replaces ASP.NET claims when specified
return yamlClaims;
}
```
---
## Request Dispatch
### Dispatch Flow
```
RequestFrame (from Router)
┌───────────────────────────────────────┐
│ 1. Create DI Scope │
│ var scope = CreateAsyncScope() │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 2. Build HttpContext │
│ • Method, Path, QueryString │
│ • Headers (including identity) │
│ • Body stream │
│ • RequestServices = scope.Provider │
│ • CancellationToken wiring │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 3. Match Endpoint │
│ Use ASP.NET EndpointSelector │
│ Preserves constraints/precedence │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 4. Populate Identity │
│ Map X-StellaOps-* headers to │
│ ClaimsPrincipal on HttpContext │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 5. Execute RequestDelegate │
│ Runs full ASP.NET pipeline: │
│ • Endpoint filters │
│ • Authorization filters │
│ • Model binding │
│ • Handler execution │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 6. Capture Response │
│ • Status code │
│ • Headers (filtered) │
│ • Body bytes (buffered) │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 7. Dispose Scope │
│ await scope.DisposeAsync() │
└───────────────────────────────────────┘
ResponseFrame (to Router)
```
### HttpContext Construction
```csharp
public async Task<ResponseFrame> DispatchAsync(
RequestFrame request,
CancellationToken cancellationToken)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var httpContext = new DefaultHttpContext
{
RequestServices = scope.ServiceProvider
};
// Link cancellation
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
httpContext.RequestAborted);
httpContext.RequestAborted = linkedCts.Token;
// Populate request
var httpRequest = httpContext.Request;
httpRequest.Method = request.Method;
(httpRequest.Path, httpRequest.QueryString) = ParsePathAndQuery(request.Path);
foreach (var (key, value) in request.Headers)
{
httpRequest.Headers[key] = value;
}
if (request.Body is { Length: > 0 })
{
httpRequest.Body = new MemoryStream(request.Body);
httpRequest.ContentLength = request.Body.Length;
}
// Set trace identifier
httpContext.TraceIdentifier = request.CorrelationId;
// Populate identity from headers
PopulateIdentity(httpContext, request.Headers);
// Match and execute endpoint
var endpoint = await MatchEndpointAsync(httpContext);
if (endpoint is null)
{
return CreateNotFoundResponse(request.CorrelationId);
}
httpContext.SetEndpoint(endpoint);
// Capture response
var responseBody = new MemoryStream();
httpContext.Response.Body = responseBody;
try
{
await endpoint.RequestDelegate!(httpContext);
}
catch (Exception ex)
{
return CreateErrorResponse(request.CorrelationId, ex);
}
// Build response frame
return new ResponseFrame
{
CorrelationId = request.CorrelationId,
StatusCode = httpContext.Response.StatusCode,
Headers = CaptureResponseHeaders(httpContext.Response),
Body = responseBody.ToArray()
};
}
```
### Identity Population
Map Gateway-provided identity headers to `ClaimsPrincipal`:
```csharp
private void PopulateIdentity(HttpContext httpContext, IReadOnlyDictionary<string, string> headers)
{
var claims = new List<Claim>();
if (headers.TryGetValue("X-StellaOps-Actor", out var actor) && !string.IsNullOrEmpty(actor))
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, actor));
claims.Add(new Claim(StellaOpsClaimTypes.Subject, actor));
}
if (headers.TryGetValue("X-StellaOps-Tenant", out var tenant) && !string.IsNullOrEmpty(tenant))
{
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenant));
}
if (headers.TryGetValue("X-StellaOps-Scopes", out var scopes) && !string.IsNullOrEmpty(scopes))
{
foreach (var scope in scopes.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope));
}
}
if (claims.Count > 0)
{
var identity = new ClaimsIdentity(claims, "StellaRouter");
httpContext.User = new ClaimsPrincipal(identity);
}
}
```
---
## Program.cs Integration
### Service Registration
```csharp
public static class StellaRouterBridgeExtensions
{
public static IServiceCollection AddStellaRouterBridge(
this IServiceCollection services,
Action<StellaRouterBridgeOptions> configure)
{
var options = new StellaRouterBridgeOptions
{
ServiceName = "",
Version = "",
Region = ""
};
configure(options);
ValidateOptions(options);
services.AddSingleton(options);
services.AddSingleton<IAuthorizationClaimMapper, DefaultAuthorizationClaimMapper>();
services.AddSingleton<IAspNetEndpointDiscoveryProvider, AspNetCoreEndpointDiscoveryProvider>();
services.AddSingleton<IAspNetRouterRequestDispatcher, AspNetRouterRequestDispatcher>();
// Register as IEndpointDiscoveryProvider for Router SDK integration
services.AddSingleton<IEndpointDiscoveryProvider>(sp =>
sp.GetRequiredService<IAspNetEndpointDiscoveryProvider>());
// Wire into Router SDK
services.AddStellaMicroservice(microserviceOptions =>
{
microserviceOptions.ServiceName = options.ServiceName;
microserviceOptions.Version = options.Version;
microserviceOptions.Region = options.Region;
microserviceOptions.InstanceId = options.InstanceId ?? Guid.NewGuid().ToString();
});
return services;
}
public static IApplicationBuilder UseStellaRouterBridge(this IApplicationBuilder app)
{
// Ensure EndpointDataSource is available (after UseRouting)
var endpointDataSource = app.ApplicationServices
.GetService<EndpointDataSource>()
?? throw new InvalidOperationException(
"UseStellaRouterBridge must be called after UseRouting()");
// Discovery happens on first Router HELLO
// Dispatch is handled by Router SDK
return app;
}
}
```
---
## YAML Override Format
The existing `microservice.yaml` format is extended:
```yaml
microservice:
serviceName: scanner
version: "1.0.0"
region: "${REGION:default}"
endpoints:
# Override by method + path
- method: POST
path: /api/reports
timeoutSeconds: 60
supportsStreaming: false
requiringClaims:
- type: "scanner.reports.read"
# Replaces any ASP.NET-derived claims for this endpoint
# Endpoint with no authorization (explicitly allow authenticated)
- method: GET
path: /api/health
requiringClaims: [] # Empty = authenticated only, no specific claims
# Override specific claim type mapping
- method: DELETE
path: /api/scans/{id}
requiringClaims:
- type: "role"
value: "scanner-admin"
- type: "scanner.scans.delete"
```
---
## ASP.NET Feature Support Matrix
### Fully Supported
| Feature | Discovery | Dispatch | Notes |
|---------|-----------|----------|-------|
| Minimal APIs (`MapGet`, etc.) | ✓ | ✓ | Primary use case |
| Controllers (`[ApiController]`) | ✓ | ✓ | Full support |
| Route groups (`MapGroup`) | ✓ | ✓ | Path composition |
| `[Authorize]` attribute | ✓ | ✓ | Claims extraction |
| `[AllowAnonymous]` | ✓ | ✓ | Explicit anonymous |
| `.RequireAuthorization()` | ✓ | ✓ | Policy resolution |
| `[FromBody]` binding | ✓ (type) | ✓ | JSON deserialization |
| `[FromRoute]` binding | ✓ | ✓ | Path parameters |
| `[FromQuery]` binding | ✓ | ✓ | Query parameters |
| `[FromHeader]` binding | ✓ | ✓ | Header values |
| `[FromServices]` injection | N/A | ✓ | DI resolution |
| `.Produces<T>()` | ✓ | N/A | Schema metadata |
| `.WithName()` / `.WithSummary()` | ✓ | N/A | OpenAPI metadata |
| `.WithTags()` | ✓ | N/A | Grouping |
| Endpoint filters | N/A | ✓ | Filter pipeline |
| `CancellationToken` | N/A | ✓ | From Router frame |
| Route constraints (`{id:int}`) | ✓ (stripped) | ✓ | ASP.NET matcher |
| Catch-all routes (`{**path}`) | ✓ | ✓ | Normalized |
### Not Supported (v0.1)
| Feature | Reason | Workaround |
|---------|--------|------------|
| SignalR hubs | Different protocol | Use native ASP.NET |
| gRPC services | Different protocol | Use native gRPC |
| Streaming request bodies | SDK limitation | Use `IRawStellaEndpoint` |
| Custom constraints | Complexity | Use standard constraints |
| API versioning (header/query) | Complexity | Path-based versioning |
| `IFormFile` uploads | Not buffered | Use raw endpoint |
---
## Error Handling
### Discovery Errors
| Condition | Behavior | Configuration |
|-----------|----------|---------------|
| No authorization metadata | Fail discovery | `OnMissingAuthorization = RequireExplicit` |
| Unsupported constraint | Log warning, strip | `OnUnsupportedConstraint = WarnAndStrip` |
| Duplicate endpoints | Log warning, keep first | Always |
| Invalid route pattern | Skip endpoint, log error | Always |
### Dispatch Errors
| Condition | Response |
|-----------|----------|
| No matching endpoint | 404 Not Found |
| Authorization failure | 403 Forbidden |
| Model binding failure | 400 Bad Request |
| Handler exception | 500 Internal Server Error |
| Cancellation | No response (connection closed) |
---
## Testing Strategy
### Unit Tests
1. **Discovery determinism**: Same endpoints → same descriptor order
2. **Route normalization**: Constraints stripped, paths normalized
3. **Authorization mapping**: Policies → claims correctly
4. **Metadata extraction**: All ASP.NET metadata captured
### Integration Tests
1. **Minimal API dispatch**: Route parameters, query, body binding
2. **Controller dispatch**: Attribute routing, model binding
3. **Authorization flow**: Claims checked, 403 on failure
4. **Filter execution**: Endpoint filters run correctly
5. **Error mapping**: Exceptions → correct status codes
### End-to-End Tests
1. **HELLO registration**: Bridge endpoints appear in Gateway
2. **Gateway routing**: HTTP request → Router → ASP.NET → response
3. **OpenAPI aggregation**: Bridged endpoints in Gateway OpenAPI
---
## Migration Guide
### From HTTP-Only Service
```csharp
// Before: HTTP only
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
await app.RunAsync();
// After: HTTP + Router bridge
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddStellaRouterBridge(options =>
{
options.ServiceName = "myservice";
options.Version = "1.0.0";
options.Region = "default";
});
builder.Services.AddMessagingTransportClient(); // Add transport
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaRouterBridge(); // Enable bridge
app.MapControllers();
await app.RunAsync();
```
### From Dual Registration (HTTP + `[StellaEndpoint]`)
1. Remove `[StellaEndpoint]` handler classes
2. Add `AddStellaRouterBridge()` configuration
3. Add `UseStellaRouterBridge()` middleware
4. Add/update `microservice.yaml` for claim overrides
5. Remove duplicate endpoint registrations
---
## Related Documents
- Router architecture: `docs/modules/router/architecture.md`
- Migration guide: `docs/modules/router/migration-guide.md`
- Gateway identity policy: `docs/modules/gateway/identity-header-policy.md`
- Implementation sprint: `docs/implplan/SPRINT_8100_0011_0001_router_sdk_aspnet_bridge.md`

View File

@@ -0,0 +1,75 @@
# Router · Messaging Transport over Valkey (Draft v0.1)
## Status
- Draft; intended for implementation via a dedicated sprint.
- Last updated: 2025-12-23 (UTC).
## Purpose
Enable Gateway ↔ microservice Router traffic over an offline-friendly, Redis-compatible transport (Valkey) by using the existing **Messaging** transport layer:
- Router transport: `StellaOps.Router.Transport.Messaging`
- Messaging backend: `StellaOps.Messaging.Transport.Valkey`
This supports environments where direct TCP/TLS microservice connections are undesirable, and where an internal message bus is the preferred control plane.
## What Exists Today (Repository Reality)
- Messaging transport server/client:
- `src/__Libraries/StellaOps.Router.Transport.Messaging/MessagingTransportServer.cs`
- `src/__Libraries/StellaOps.Router.Transport.Messaging/MessagingTransportClient.cs`
- Valkey-backed message queue factory:
- `src/__Libraries/StellaOps.Messaging.Transport.Valkey/ValkeyMessageQueueFactory.cs`
- Gateway WebService currently starts only TCP/TLS transport servers:
- `src/Gateway/StellaOps.Gateway.WebService/Program.cs`
- `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs`
## High-Level Flow
1) Microservice connects via messaging transport:
- publishes a HELLO message to the gateway request queue
2) Gateway processes HELLO:
- registers instance + endpoints into routing state
3) Gateway routes an HTTP request to a microservice:
- publishes a REQUEST message to the service request queue
4) Microservice handles request:
- executes handler (or ASP.NET bridge) and publishes a RESPONSE message
5) Gateway returns response to the client.
## Queue Topology (Conceptual)
The Messaging transport uses a small set of queues (names are configurable):
- **Gateway request queue**: receives HELLO / HEARTBEAT / REQUEST frames from services
- **Gateway response queue**: receives RESPONSE frames from services
- **Per-service request queues**: gateway publishes REQUEST frames targeted to a service
- **Dead letter queues** (optional): for messages that exceed retries/leases
## Configuration (Draft)
### Gateway
- Register Valkey messaging services (`StellaOps.Messaging.Transport.Valkey`)
- Add messaging transport server (`AddMessagingTransportServer`)
- Add Gateway config section for messaging transport options:
- Valkey connection info (host/port/auth)
- queue naming prefix
- consumer group / lease duration / dead-letter suffix
### Microservice
- Register Valkey messaging services (`StellaOps.Messaging.Transport.Valkey`)
- Add messaging transport client (`AddMessagingTransportClient`)
- Ensure Microservice Router SDK connects via `IMicroserviceTransport`
## Operational Semantics (Draft)
- **At-least-once** delivery: message queues and leases imply retries are possible; handlers should be idempotent where feasible.
- **Lease timeouts**: must be tuned to max handler execution time; long-running tasks should respond with 202 + job id rather than blocking.
- **Determinism**: message ordering may vary; Router must not depend on arrival order for correctness (only for freshness/telemetry).
## Security Notes
- Messaging transport is internal. External identity must still be enforced at the Gateway.
- The Gateway must not trust client-supplied identity headers; it must overwrite reserved headers before dispatch.
- See `docs/modules/gateway/identity-header-policy.md`.
## Gaps / Implementation Work
1) Wire Messaging transport into Gateway:
- start/stop `MessagingTransportServer`
- subscribe to HELLO/HEARTBEAT/RESPONSE events and reuse existing HELLO validation + routing state updates
2) Extend Gateway transport client to support `TransportType.Messaging` for dispatch.
3) Add config mapping and deployment examples (compose/helm) for Valkey transport.
4) Add integration tests covering:
- microservice HELLO registration via messaging
- request dispatch + response return

View File

@@ -0,0 +1,120 @@
# Stella Router ASP.NET WebService Integration Guide
This guide explains how to integrate any ASP.NET Core WebService with the Stella Router for automatic endpoint discovery and dispatch.
## Prerequisites
Add a project reference to `StellaOps.Router.AspNet`:
```xml
<ProjectReference Include="../../__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
```
## Integration Steps
### 1. Add Router Options to Service Options
In your service's options class (e.g., `MyServiceOptions.cs`), add:
```csharp
using StellaOps.Router.AspNet;
public class MyServiceOptions
{
// ... existing options ...
/// <summary>
/// Stella Router integration configuration (disabled by default).
/// </summary>
public StellaRouterOptionsBase? Router { get; set; }
}
```
### 2. Register Services in Program.cs
Add the using directive:
```csharp
using StellaOps.Router.AspNet;
```
After service registration (e.g., after `AddControllers()`), add:
```csharp
// Stella Router integration
builder.Services.TryAddStellaRouter(
serviceName: "my-service-name",
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
routerOptions: options.Router);
```
### 3. Enable Middleware
After `UseAuthorization()`, add:
```csharp
app.TryUseStellaRouter(resolvedOptions.Router);
```
### 4. Refresh Endpoint Cache
After all endpoints are mapped (before `app.RunAsync()`), add:
```csharp
app.TryRefreshStellaRouterEndpoints(resolvedOptions.Router);
```
## Configuration Example (YAML)
```yaml
myservice:
router:
enabled: true
region: "us-east-1"
defaultTimeoutSeconds: 30
heartbeatIntervalSeconds: 10
gateways:
- host: "router.stellaops.local"
port: 9100
transportType: "Tcp"
useTls: true
certificatePath: "/etc/certs/router.pem"
```
## WebServices Requiring Integration
The following WebServices need to be updated with Router integration:
| Service | Path | Status |
|---------|------|--------|
| Scanner.WebService | `src/Scanner/StellaOps.Scanner.WebService` | ✅ Complete |
| Concelier.WebService | `src/Concelier/StellaOps.Concelier.WebService` | Pending |
| Excititor.WebService | `src/Excititor/StellaOps.Excititor.WebService` | Pending |
| Gateway.WebService | `src/Gateway/StellaOps.Gateway.WebService` | Pending |
| VexHub.WebService | `src/VexHub/StellaOps.VexHub.WebService` | Pending |
| Attestor.WebService | `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService` | Pending |
| EvidenceLocker.WebService | `src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.WebService` | Pending |
| Findings.Ledger.WebService | `src/Findings/StellaOps.Findings.Ledger.WebService` | Pending |
| AdvisoryAI.WebService | `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService` | Pending |
| IssuerDirectory.WebService | `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService` | Pending |
| Notifier.WebService | `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService` | Pending |
| Notify.WebService | `src/Notify/StellaOps.Notify.WebService` | Pending |
| PacksRegistry.WebService | `src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService` | Pending |
| RiskEngine.WebService | `src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService` | Pending |
| Signer.WebService | `src/Signer/StellaOps.Signer/StellaOps.Signer.WebService` | Pending |
| TaskRunner.WebService | `src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService` | Pending |
| TimelineIndexer.WebService | `src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService` | Pending |
| Orchestrator.WebService | `src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService` | Pending |
| Scheduler.WebService | `src/Scheduler/StellaOps.Scheduler.WebService` | Pending |
| ExportCenter.WebService | `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService` | Pending |
## Files Created
The Router.AspNet library includes the following files:
- `StellaOps.Router.AspNet.csproj` - Project file
- `StellaRouterOptions.cs` - Unified router options
- `StellaRouterExtensions.cs` - DI extensions (`AddStellaRouter`, `UseStellaRouter`)
- `CompositeRequestDispatcher.cs` - Routes requests to ASP.NET or Stella endpoints
- `StellaRouterOptionsBase.cs` - Base options class for embedding in service options
- `StellaRouterIntegrationHelper.cs` - Helper methods for conditional integration