feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s

Closes #7
This commit is contained in:
KyuubiYoru
2026-07-16 05:58:47 +02:00
parent 02ca502a76
commit 49564c7e7e
25 changed files with 2069 additions and 46 deletions
+59 -2
View File
@@ -2,6 +2,7 @@ using System.Net;
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.Http;
using FinalFactory.Rendezvous.Server.Provisioning;
using FinalFactory.Rendezvous.Server.Sessions;
using FinalFactory.Rendezvous.Server.State;
using FinalFactory.Rendezvous.Server.Transport;
using Microsoft.OpenApi;
@@ -13,6 +14,7 @@ bool isOpenApiGeneration = string.Equals(
StringComparison.Ordinal);
builder.Services.AddOpenApi("v1", static options =>
{
options.AddSchemaTransformer(static (schema, context, cancellationToken) =>
{
Type type = context.JsonTypeInfo.Type;
@@ -32,16 +34,65 @@ builder.Services.AddOpenApi("v1", static options =>
}
return Task.CompletedTask;
}));
});
options.AddDocumentTransformer(static (document, context, cancellationToken) =>
{
const string schemeName = "PublisherBearer";
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??=
new Dictionary<string, IOpenApiSecurityScheme>(StringComparer.Ordinal);
document.Components.SecuritySchemes[schemeName] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "rv1 publisher credential",
Description = "Tenant-scoped publisher credential issued during game provisioning.",
};
HashSet<string> securedOperations = new(StringComparer.Ordinal)
{
"RegisterSession",
"RenewSessionLease",
"UpdateSession",
"DeleteSession",
};
OpenApiSecuritySchemeReference reference = new(schemeName, document, null);
foreach (OpenApiPathItem path in document.Paths.Values)
{
if (path.Operations is null)
{
continue;
}
foreach (OpenApiOperation operation in path.Operations.Values.Where(
operation => securedOperations.Contains(operation.OperationId ?? string.Empty)))
{
operation.Security ??= [];
operation.Security.Add(new OpenApiSecurityRequirement
{
[reference] = [],
});
}
}
return Task.CompletedTask;
});
});
builder.Services.ConfigureHttpJsonOptions(static options =>
ContractJson.Configure(options.SerializerOptions));
builder.Services.Configure<RouteHandlerOptions>(static options =>
options.ThrowOnBadRequest = true);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<RendezvousExceptionHandler>();
SystemRendezvousClock rendezvousClock = new();
EphemeralStoreOptions stateOptions = new();
InMemoryEphemeralRendezvousStore stateStore = new(
new EphemeralStoreOptions(),
stateOptions,
rendezvousClock,
rendezvousClock);
builder.Services.AddSingleton<IEphemeralRendezvousStore>(stateStore);
builder.Services.AddSingleton<IWallClock>(rendezvousClock);
if (isOpenApiGeneration)
{
@@ -63,6 +114,11 @@ else
builder.Services.AddSingleton(provisioning.Policies);
builder.Services.AddSingleton(provisioning.Credentials);
builder.Services.AddSingleton(provisioning.PublisherAuthorization);
EphemeralCapabilityIssuer sessionCapabilities = new();
builder.Services.AddSingleton(sessionCapabilities);
builder.Services.AddSingleton<ISessionCapabilityService>(sessionCapabilities);
builder.Services.AddSingleton(SessionLeaseTiming.From(stateOptions));
builder.Services.AddSingleton<SessionLeaseService>();
builder.Services.AddSingleton(new ProvisioningReadiness(true));
}
@@ -84,6 +140,7 @@ if (!isOpenApiGeneration)
WebApplication app = builder.Build();
app.Lifetime.ApplicationStopping.Register(() => stateStore.BeginDrain());
app.UseExceptionHandler();
app.MapOpenApi();
app.MapRendezvousContractEndpoints();
app.MapGet(