API reference — Clients

Nuvora.Nexus.Sentinel.Client

.NET client for the Sentinel HTTP surface: typed auth + profile calls over Bearer or cookie transport, in-memory token handling with auto-refresh, API-key mode for machines, a DelegatingHandler for service-to-service calls, and client-side can() running the same golden-vector-tested evaluator as the server.

dotnet add package Nuvora.Nexus.Sentinel.Client

Nuvora.Nexus.Sentinel.Client

ISentinelTokenStore

public interface ISentinelTokenStore

Where Bearer-mode tokens live (the Blazor seam). The default is InMemorySentinelTokenStore — memory only, which is the right call for browsers: anything a page can persist, injected script can steal, so persisted bearer tokens turn any XSS into durable offline credential theft. Blazor WASM apps that accept that tradeoff (or server-side apps with protected storage) can plug their own implementation; the contract is async precisely so implementations can await IJSRuntime / ProtectedBrowserStorage. This package deliberately references no Blazor assemblies — it is only the seam.

Implementations must make ISentinelTokenStore.SetAsync an atomic swap of the whole pair — the client and SentinelAuthHandler may race a refresh, and a torn access/refresh pair would strand the session.

Methods

ValueTask SetAsync(SentinelTokenPair? tokens, CancellationToken cancellationToken = default(CancellationToken))

Atomically replaces the held pair; null clears it (logout).

ValueTask<SentinelTokenPair?> GetAsync(CancellationToken cancellationToken = default(CancellationToken))

The currently held pair, or null when logged out.

InMemorySentinelTokenStore

public sealed class InMemorySentinelTokenStore : ISentinelTokenStore

Default store: a single volatile reference — atomic swaps, no persistence, no locks.

Methods

ValueTask SetAsync(SentinelTokenPair? tokens, CancellationToken cancellationToken = default(CancellationToken))

Atomically replaces the held pair; null clears it (logout).

ValueTask<SentinelTokenPair?> GetAsync(CancellationToken cancellationToken = default(CancellationToken))

The currently held pair, or null when logged out.

SentinelAuthHandler

public sealed class SentinelAuthHandler : DelegatingHandler

The service-to-service story: a DelegatingHandler that attaches the current Sentinel access token (or a static API key) to ANY outgoing request and performs the refresh-once dance on 401 — so an app registers this handler on the HttpClient for its OWN Sentinel-protected API, while SentinelClient (sharing the same ISentinelTokenStore) handles the login flow:

services.AddSentinelClient(o => { o.BaseUrl = "https://idp.example.com"; });
services.AddHttpClient("my-api", c => c.BaseAddress = new Uri("https://api.example.com"))
        .AddHttpMessageHandler<SentinelAuthHandler>();

SentinelClientOptions.BaseUrl MUST be absolute here: the refresh request is issued through this handler’s own inner chain, whose client BaseAddress points at the protected API, not at Sentinel. Refresh failures are swallowed and the ORIGINAL 401 is returned — token maintenance must never mask the real response.

Constructors

SentinelAuthHandler(SentinelClientOptions options, ISentinelTokenStore tokenStore)

Methods

override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.

SentinelClient

public sealed class SentinelClient

Typed client for the Sentinel HTTP surface, over the mountable endpoint groups MapSentinelAuth + MapSentinelProfile. Designed as an IHttpClientFactory typed client (see AddSentinelClient) but constructible by hand.

Transports: SentinelClientTransport.Bearer keeps tokens in an ISentinelTokenStore (in-memory by default) and auto-refreshes ONCE on a 401 before retrying; SentinelClientTransport.Cookie relies on the handler’s cookie container and echoes the CSRF double-submit header on unsafe methods. An SentinelClientOptions.ApiKey pins a static Bearer snt_… header with no refresh flow. Instance state (cached user/permissions/org context) uses atomic reference swaps; token state is delegated to the store, so the client is safe for concurrent calls.

Constructors

SentinelClient(HttpClient httpClient, SentinelClientOptions options, ISentinelTokenStore? tokenStore = null)

Properties

ISentinelTokenStore TokenStore { get; }

The token store backing Bearer mode — share it with SentinelAuthHandler for the service-to-service story.

SentinelPermissionSet? CurrentPermissions { get; }

The permission snapshot from the last SentinelClient.PermissionsAsync, or null before the first fetch.

SentinelUser? CurrentUser { get; }

The user from the last SentinelClient.MeAsync, or null before the first fetch.

Methods

Task LogoutAllAsync(CancellationToken cancellationToken = default(CancellationToken))

Ends EVERY session for the subject — remote logout of all devices — then drops local state.

Task LogoutAsync(CancellationToken cancellationToken = default(CancellationToken))

Ends the current session (and its refresh family) on the server, then drops all local state.

Task RefreshAsync(CancellationToken cancellationToken = default(CancellationToken))

Explicitly rotates the refresh token. Bearer mode sends the stored refresh token; cookie mode sends no body and the server falls back to the path-scoped refresh cookie. Rarely needed directly — authenticated calls already refresh-once on 401 automatically.

Task RevokeSessionAsync(Guid sessionId, CancellationToken cancellationToken = default(CancellationToken))

Remote logout of one session. Foreign/unknown ids surface as SentinelErrorCodes.NotFound.

Task<IReadOnlyList<SentinelSession>> SessionsAsync(CancellationToken cancellationToken = default(CancellationToken))

Active device sessions of the caller.

Task<SentinelLoginResult> LoginAsync(string email, string password, Guid? organizationId = null, CancellationToken cancellationToken = default(CancellationToken))

Password login. Expected outcomes come back as a typed SentinelLoginResult (Success / MfaRequired / Blocked / InvalidCredentials); only transport failures and unexpected statuses throw. On success in Bearer mode the tokens land in the store; on an org-less login that resolves to exactly one organization, that org becomes the Can() org context.

Task<SentinelLoginResult> VerifyRecoveryCodeAsync(string mfaPendingToken, string code, CancellationToken cancellationToken = default(CancellationToken))

Completes an MFA-gated login with a recovery code.

Task<SentinelLoginResult> VerifyTotpAsync(string mfaPendingToken, string code, CancellationToken cancellationToken = default(CancellationToken))

Completes an MFA-gated login with a TOTP code.

Task<SentinelPermissionSet> PermissionsAsync(CancellationToken cancellationToken = default(CancellationToken))

Fetches the redacted permission snapshot (allow patterns + team ids only — deny grants never reach the client) and caches it for SentinelClient.Can.

Task<SentinelUser> MeAsync(CancellationToken cancellationToken = default(CancellationToken))

The caller’s identity; cached so SentinelClient.Can knows the subject id for self-scoped checks.

bool Can(string permission, SentinelResourceContext? resource = null)

Client-side permission check over the cached redacted snapshot, running the exact golden-vector-pinned evaluator the server uses. Returns false (fail closed) until SentinelClient.PermissionsAsync has been called. Because the snapshot is redacted, true is optimistic UI guidance — the server may still deny.

void SetCsrfToken(string? token)

Sets the CSRF double-submit value the client echoes into SentinelClientOptions.CsrfHeaderName on unsafe cookie-transport requests. Normally unnecessary — the client captures it from Set-Cookie response headers — but a host app that reads the cookie itself (e.g. server-rendered pages) can push it here.

void SetOrganizationContext(Guid? organizationId)

Selects the org context used by SentinelClient.Can for org-scoped checks.

SentinelClientException

public sealed class SentinelClientException : Exception

Structured failure carrying the HTTP status and the stable machine error code from the server’s problem+json — the .NET twin of the TypeScript client’s SentinelError.

Constructors

SentinelClientException(string message, int statusCode, string? errorCode)

Properties

int StatusCode { get; }

The HTTP status code of the failed response.

string? ErrorCode { get; }

Stable machine code (SentinelErrorCodes) or null when the body carried none.

SentinelClientOptions

public sealed class SentinelClientOptions

Configuration for SentinelClient and SentinelAuthHandler.

Properties

SentinelClientTransport Transport { get; set; }

Credential transport. Default SentinelClientTransport.Bearer.

string AuthPrefix { get; set; }

Prefix MapSentinelAuth was mounted at. Default /auth.

string CsrfCookieName { get; set; }

Cookie-transport CSRF cookie name; must match SentinelAspNetOptions.CsrfCookieName.

string CsrfHeaderName { get; set; }

Cookie-transport CSRF header name; must match SentinelAspNetOptions.CsrfHeaderName.

string ProfilePrefix { get; set; }

Prefix MapSentinelProfile was mounted at. Default /profile.

string? ApiKey { get; set; }

Machine credential: a full snt_… API key. When set, every request carries a static Authorization: Bearer snt_… header and the refresh flow is disabled — API keys don’t rotate through /auth/refresh.

string? BaseUrl { get; set; }

Origin (and optional path base) the Sentinel endpoint groups are mounted under, e.g. https://idp.example.com. Optional for SentinelClient when the underlying HttpClient.BaseAddress is set; REQUIRED for SentinelAuthHandler refresh support (the handler builds absolute refresh URIs because it may be mounted on a client whose BaseAddress points at YOUR API, not Sentinel).

SentinelClientTransport

public enum SentinelClientTransport

How the client presents credentials — both transports are first-class.

Values

  • Bearer — Tokens are held IN MEMORY (via ISentinelTokenStore) and sent as Authorization: Bearer. The mode for APIs, machines, tests, and Blazor apps that accept memory-only tokens. Never persist bearer tokens to browser storage — anything a page can read, injected script can read.
  • Cookie — Tokens live in httpOnly cookies managed by the HttpClientHandler’s cookie container; the client never sees them. Unsafe requests echo the CSRF double-submit cookie into SentinelClientOptions.CsrfHeaderName — the client reads the CSRF value from Set-Cookie response headers automatically, or the host app can push it via SentinelClient.SetCsrfToken.

SentinelErrorCodes

public static class SentinelErrorCodes

The stable machine codes the server puts in its problem+json bodies. Mirrors SentinelProblems.Codes on the server — renaming one there is a breaking change.

Fields

const string Blocked = "blocked"
const string InvalidCredentials = "invalid_credentials"
const string InvalidRefreshToken = "invalid_refresh_token"
const string InvalidRequest = "invalid_request"
const string NotFound = "not_found"
const string Unauthenticated = "unauthenticated"

SentinelLoginResult

public sealed record SentinelLoginResult : IEquatable<SentinelLoginResult>

Typed login outcome. Only transport/credential failures throw; expected outcomes are values.

Constructors

SentinelLoginResult(SentinelLoginStatus Status, string? MfaPendingToken = null, IReadOnlyList<Guid>? OrganizationIds = null)

Typed login outcome. Only transport/credential failures throw; expected outcomes are values.

Properties

IReadOnlyList<Guid>? OrganizationIds { get; init; }
SentinelLoginStatus Status { get; init; }
string? MfaPendingToken { get; init; }

SentinelLoginStatus

public enum SentinelLoginStatus

Outcome of a login or MFA-verify call, as a typed result instead of exceptions.

Values

SentinelPermissionGrant

public sealed record SentinelPermissionGrant : IEquatable<SentinelPermissionGrant>

One entry of the redacted snapshot: a pattern string and its effect (today always allow).

Constructors

SentinelPermissionGrant(string Pattern, string Effect)

One entry of the redacted snapshot: a pattern string and its effect (today always allow).

Properties

string Effect { get; init; }
string Pattern { get; init; }

SentinelPermissionSet

public sealed class SentinelPermissionSet

The redacted permission snapshot (allow patterns + team ids only) with a SentinelPermissionSet.Can that runs Core’s golden-vector-pinned matcher and deny-overrides evaluator — the exact code the server runs, not a copy.

Because the snapshot is redacted (deny grants, conditions, and org scoping never reach the client), a true here is optimistic UI guidance — the server, denies included, remains the only authority. A false is reliable: Can never widens beyond what the evaluator allows for the snapshot it is given.

Constructors

SentinelPermissionSet(IReadOnlyList<SentinelPermissionGrant> grants, IReadOnlyList<Guid> teams)

Properties

IReadOnlyList<Guid> Teams { get; }

Team ids of the subject within its organization context.

IReadOnlyList<SentinelPermissionGrant> Grants { get; }

The raw redacted entries, as received from the server.

Methods

bool Can(string permission, SentinelResourceContext? resource = null)

Context-free check: no subject id, no organization context (org-scoped grants that need an org fail closed).

bool Can(string permission, SentinelResourceContext? resource, Guid subjectId, Guid? organizationId)

Full check: permission must be a concrete service:scope:action id — a malformed one throws FormatException (a typo in a permission string is a programming error, not a deny). subjectId feeds self-scoped checks; organizationId is the org context of the caller’s session.

SentinelResourceContext

public sealed record SentinelResourceContext : IEquatable<SentinelResourceContext>

Optional resource/context data for a Can() check; what is omitted cannot satisfy its scope — e.g. a team-scoped permission checked without SentinelResourceContext.TeamIds fails closed.

Constructors

SentinelResourceContext(Guid? OrganizationId = null, IReadOnlyList<Guid>? TeamIds = null, Guid? OwnerId = null, IReadOnlyDictionary<string, object?>? Attributes = null, IReadOnlyDictionary<string, object?>? Context = null)

Optional resource/context data for a Can() check; what is omitted cannot satisfy its scope — e.g. a team-scoped permission checked without SentinelResourceContext.TeamIds fails closed.

Properties

Guid? OrganizationId { get; init; }
Guid? OwnerId { get; init; }
IReadOnlyDictionary<string, object?>? Attributes { get; init; }
IReadOnlyDictionary<string, object?>? Context { get; init; }
IReadOnlyList<Guid>? TeamIds { get; init; }

SentinelSession

public sealed record SentinelSession : IEquatable<SentinelSession>

One active device session (GET {profile}/sessions).

Constructors

SentinelSession(Guid Id, DateTimeOffset CreatedAt, DateTimeOffset LastSeenAt, string? Device, bool Current)

One active device session (GET {profile}/sessions).

Properties

DateTimeOffset CreatedAt { get; init; }
DateTimeOffset LastSeenAt { get; init; }
Guid Id { get; init; }
bool Current { get; init; }
string? Device { get; init; }

SentinelTokenPair

public sealed record SentinelTokenPair : IEquatable<SentinelTokenPair>

An access/refresh token pair held by an ISentinelTokenStore.

Constructors

SentinelTokenPair(string AccessToken, string? RefreshToken)

An access/refresh token pair held by an ISentinelTokenStore.

Properties

string AccessToken { get; init; }
string? RefreshToken { get; init; }

SentinelUser

public sealed record SentinelUser : IEquatable<SentinelUser>

The caller’s identity (GET {profile}/me).

Constructors

SentinelUser(Guid Id, string Email, string? DisplayName, bool EmailVerified, IReadOnlyList<Guid> OrganizationIds)

The caller’s identity (GET {profile}/me).

Properties

Guid Id { get; init; }
IReadOnlyList<Guid> OrganizationIds { get; init; }
bool EmailVerified { get; init; }
string Email { get; init; }
string? DisplayName { get; init; }

Nuvora.Nexus.Sentinel.Client.DependencyInjection

SentinelClientServiceCollectionExtensions

public static class SentinelClientServiceCollectionExtensions

Methods

static IHttpClientBuilder AddSentinelClient(this IServiceCollection services, Action<SentinelClientOptions> configure)

Registers SentinelClient as an IHttpClientFactory typed client, plus the pieces the service-to-service story needs: ISentinelTokenStore — singleton in-memory default (TryAdd: register your own BEFORE this call to plug persistence, e.g. from a Blazor WASM app).; SentinelAuthHandler — transient, sharing the store and ONE refresh gate with the typed client; attach it to any other HttpClient via .AddHttpMessageHandler<SentinelAuthHandler>() to call your own Sentinel-protected APIs. Returns the IHttpClientBuilder so hosts can chain handler/primary-handler configuration (Cookie transport relies on the primary handler’s default cookie container).