using System.Reflection;
using DodoSSH.Api.Setup;
using DodoSSH.Contracts;
using DodoSSH.Crypto;
using FastEndpoints;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
namespace DodoSSH.Api.Features.Meta;
///
/// Capability and discovery endpoints.
///
///
/// These replace URL-based API versioning. When client and server upgrade independently — which is
/// the normal case for self-hosted software — a client needs to ask what this particular server
/// supports rather than assume. See ADR 0002.
///
internal sealed class GetMetaEndpoint(
IOptions sync,
IOptions relay,
IOptions events,
IOptions server)
: EndpointWithoutRequest>
{
/// Sync semantics version. Bumped when push or pull behaviour changes.
internal const int SyncProtocolVersion = 1;
///
public override void Configure()
{
Get("/api/v1/meta");
// Anonymous by necessity: a client must be able to discover how to authenticate before it
// can authenticate. Load-bearing rather than decorative — FastEndpoints authenticates every
// endpoint that does not say otherwise, so removing this line is a 401 nobody can recover from.
AllowAnonymous();
Description(b => b
.WithName("GetMeta")
.WithSummary("Server capabilities, versions and limits."));
}
///
public override Task> ExecuteAsync(CancellationToken ct)
{
List features = ["teams"];
if (relay.Value.Enabled)
{
features.Add(RelayProtocol.Feature);
}
// Advertised so a client knows whether to hold a socket open or rely on its timer. Absence is
// not an error — synchronising on a timer is the supported behaviour and the socket only makes
// it early — which is why this is a feature flag rather than a version bump. See ADR 0012.
if (events.Value.Enabled)
{
features.Add(VaultEvents.Feature);
}
return Task.FromResult(TypedResults.Ok(new MetaResponse(
ServerVersion: ServerVersion,
ApiVersions: [1],
SyncProtocolVersion: SyncProtocolVersion,
CryptoSpecVersion: CryptoSpec.CurrentAadVersion,
Features: features,
MinClientVersion: server.Value.MinClientVersion,
MaxOperationsPerPush: sync.Value.MaxOperationsPerPush,
MaxPayloadBytes: sync.Value.MaxPayloadBytes,
MaxItemPayloadBytes: sync.Value.MaxItemPayloadBytes)));
}
private static string ServerVersion { get; } =
typeof(GetMetaEndpoint).Assembly
.GetCustomAttribute()?.InformationalVersion
?? "0.0.0";
}
///
/// Everything a client needs to begin authenticating, from one URL.
///
///
/// This is the onboarding story: the user types one server URL and the client discovers the
/// identity provider, the client id and the relay from here. See ADR 0002.
///
internal sealed class GetDodoSshConfigurationEndpoint(
IOptions oidc,
IOptions relay,
IOptions server)
: EndpointWithoutRequest>
{
///
public override void Configure()
{
Get("/.well-known/dodossh-configuration");
// Anonymous for the same reason as /api/v1/meta, and more so: this is the document that says
// where the identity provider is.
AllowAnonymous();
Description(b => b
.WithName("GetDodoSshConfiguration")
.WithSummary("Everything a client needs to begin authenticating, from one URL."));
}
///
public override Task> ExecuteAsync(CancellationToken ct)
{
var relayOptions = relay.Value;
return Task.FromResult(TypedResults.Ok(new DodoSshConfiguration(
ApiBaseUrl: new Uri(server.Value.PublicBaseUrl, UriKind.Absolute),
Oidc: new OidcConfiguration(
Authority: new Uri(oidc.Value.Authority, UriKind.Absolute),
ClientId: oidc.Value.ClientId,
Scopes: [.. oidc.Value.Scopes],
LoopbackRedirectPattern: oidc.Value.LoopbackRedirectPattern),
Relay: new RelayConfiguration(
Enabled: relayOptions.Enabled,
WebSocketUrl: relayOptions.Enabled && relayOptions.WebSocketUrl is not null
? new Uri(relayOptions.WebSocketUrl, UriKind.Absolute)
: null))));
}
}