Dotwire.Host
Dotwire.Host is the .NET SDK for your host backend. It mints RS256 tokens with your private key and calls dotwire's HTTP API. It targets net8.0, net9.0, and net10.0, and depends only on Microsoft.IdentityModel.JsonWebTokens and Microsoft.IdentityModel.Tokens.
There is no .NET member client. Browsers use @dotwire/client.
Install
dotnet add package Dotwire.Host
Quickstart
var options = new DotwireHostOptions
{
BaseUrl = new Uri("http://localhost:8080"),
Issuer = "my-host",
KeyId = "host-key-1",
PrivateKeyPem = File.ReadAllText("host-jwt.key"),
};
var http = new HttpClient(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2) })
{
BaseAddress = options.BaseUrl,
};
IDotwireHostClient client = new DotwireHostClient(http, new DotwireTokenSigner(options), options);
var ack = await client.SendAsUserAsync(roomId, "user-42", "hello");
// ack.Seq is the JetStream ordering token - the handle a later redaction targets.
var participantIds = await client.GetRoomParticipantsAsync(roomId, "user-42");
// The caller must be a room member. Resolve names and avatars in the host app.
Options
DotwireHostOptions properties:
| Property | Default | Description |
|---|---|---|
BaseUrl | Required | dotwire's base URL. |
Issuer | Required | The iss claim. Must match dotwire's Auth:Issuer. |
Audience | "dotwire" | The aud claim. Must match dotwire's Auth:Audience. |
KeyId | Required | The kid header. Must match a key dotwire trusts. |
PrivateKeyPem | Required | Your RS256 private key, PEM-encoded. |
AdminUserId | "host-admin" | User id the SDK signs admin calls as. |
AuditorUserId | "host-auditor" | User id the SDK signs audit calls as. |
DefaultTokenTtl | 15 minutes | Lifetime of tokens the SDK mints internally. |
RequestTimeout | 15 seconds | Timeout for HTTP requests. |
WebhookSecret | — | HMAC secret for verifying X-Dotwire-Signature on webhook calls. |
RoleCacheTtl | 5 minutes | How long resolved roles stay cached. |
Dependency injection
The SDK doesn't reference Microsoft.Extensions.DependencyInjection.Abstractions or Microsoft.Extensions.Configuration.Abstractions, so it ships no AddDotwireHost extension method. To register it in a service collection, add those two packages to your project and write the registration yourself. The Dotwire.Host README in the repository has a complete extension method you can copy. Its configuration keys match the DotwireHostOptions property names.
Identity rules
- Role, membership, injection, redaction, DSAR, and retention calls are signed as
AdminUserId. Migrations seedhost-adminwith theadminrole. - Audit read, verify, and checkpoint calls are signed as
AuditorUserId. Migrations seedhost-auditorwith theauditorrole. - Send-as-user, history-as-user, SSE-as-user, and
HandleClientSendAsyncmint a token for that user. The SDK triesmemberfirst. On a403, it looks the role up throughGET /admin/users/{userId}/role, caches it forRoleCacheTtl, and retries once. A second403throwsDotwireApiExceptionwithKind == Forbidden. MintToken(userId, role, ...)skips role resolution: you assert the role.
Admin and auditor tokens are cached and re-minted one minute before they expire.
Methods
IDotwireHostClient exposes:
| Area | Methods |
|---|---|
| Tokens | MintToken(userId, role, displayName, ttl) |
| Roles | SetUserRoleAsync, GetUserRoleAsync, DeleteUserRoleAsync, GetUserRoomsAsync |
| Membership | AddRoomMembersAsync(roomId, userIds, ensureMemberRole = true), RemoveRoomMemberAsync, GetRoomMembersAsync, GetRoomParticipantsAsync(roomId, asUserId) |
| Messages | SendAsUserAsync(roomId, userId, content), SendSystemMessageAsync(roomId, content, senderId = "system"), GetMessagesAsync(roomId, asUserId, query) |
| Compliance | RedactMessageAsync, ExportUserAsync, GetMessageRetentionDaysAsync, SetMessageRetentionDaysAsync |
| Audit | ReadAuditLogAsync(afterId, limit), VerifyAuditLogAsync(full), GetAuditCheckpointsAsync(limit) |
| Events | StreamEventsAsync(roomId, asUserId, options) |
| Handlers | HandleClientSendAsync, HandlePresendWebhookAsync, HandlePostSendWebhookAsync |
| Hooks | Presend, PostSend |
Every async method takes an optional CancellationToken. Room ids are Guid values.
Membership
Guid roomId = Guid.Parse("a0000000-0000-0000-0000-000000000001");
await client.SetUserRoleAsync("user-42", "member");
await client.AddRoomMembersAsync(roomId, ["user-42", "user-57"]);
string token = client.MintToken("user-42", role: "member");
IReadOnlyList<string> userIds = await client.GetRoomParticipantsAsync(roomId, "user-42");
GetRoomMembersAsync(roomId) is an admin operation for host management. GetRoomParticipantsAsync(roomId, asUserId) uses that user's room access.
System messages
var ack = await client.SendSystemMessageAsync(roomId, "system announcement");
var botAck = await client.SendSystemMessageAsync(roomId, "build finished", senderId: "bot:ci");
The admin identity doesn't need to be a room member, and every injection is audited. If the audit event can't be written, dotwire erases the message before anyone receives it and the call throws DotwireApiException with Kind == Unavailable, so retrying is safe. See System messages.
Streaming events (SSE)
await foreach (var evt in client.StreamEventsAsync(roomId, asUserId: "user-42"))
{
switch (evt)
{
case MessageEvent m: Render(m); break;
case RetractedEvent r: Remove(r.Seq); break;
case TypingEvent t: break;
case PresenceEvent p: break;
case RevokedEvent: return; // membership revoked - server closed the stream, we stop
}
}
Render and Remove stand for your own code. With StreamOptions.AutoResume = true (the default), a dropped connection reconnects with Last-Event-ID set to the last message seq, backing off exponentially from 1 second to 30 seconds, and mints a fresh token for each attempt. A RevokedEvent ends the stream and never triggers a reconnect.
Compliance
var result = await client.RedactMessageAsync(roomId, seq);
// result.RemovedFromHistory, result.RemovedFromStream
var page = await client.ReadAuditLogAsync(afterId: 0, limit: 100);
var verification = await client.VerifyAuditLogAsync(full: true);
var checkpoints = await client.GetAuditCheckpointsAsync();
await client.SetMessageRetentionDaysAsync(365); // 0 = indefinite
var days = await client.GetMessageRetentionDaysAsync();
var export = await client.ExportUserAsync(userId); // buffered UserExport
await client.ExportUserAsync(userId, someStream); // streamed straight through
See Encryption and retention and Audit log.
Moderation and webhooks
Presend and PostSend run for SendAsUserAsync, HandleClientSendAsync, and the webhook handlers, but never for SendSystemMessageAsync. The SDK also includes ModerationQueue and BatchModerationWorker in Dotwire.Host.Moderation. See Moderation webhooks.
Errors
DotwireApiExceptioncarriesKind(BadRequest,Unauthorized,Forbidden,NotFound,PresendRejected,RateLimited,Unavailable, orUnknown), theErrorCodeandReasonfrom the error body when present, andRetryAfter, parsed from theRetry-Afterheader on a429.PresendRejectedExceptionis thrown bySendAsUserAsyncwhen yourPresendhook rejects a message.PostSendExceptionis thrown bySendAsUserAsyncwhen yourPostSendhook throws. It carries the ack. The message was already sent, so don't retry it.
Related
- Integrate a host app
@dotwire/host, the Node.js equivalent