Summary Update.

This commit is contained in:
2026-06-06 23:38:50 -04:00
parent dd75ca4b06
commit 2916d17868
30 changed files with 1231 additions and 21 deletions

View File

@@ -2,11 +2,24 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `channel_message_edits` table. One row per historical version of
/// an edited message — written by HandleEditMessage BEFORE overwriting the live row.
///
/// Encrypted with the channel AES key (same as ChannelMessages), so HandleGetEditHistory
/// can decrypt + re-encrypt per requester.
/// </summary>
public class ChannelMessageEdits : Record
{
/// <summary>"channel_messages:abc" — which live message this version belonged to.</summary>
public required string MessageId { get; set; }
/// <summary>Base64 AES-GCM ciphertext of the JSON-serialised previous ChatMessageContent.</summary>
public required string CipherText { get; set; }
public required string Nonce { get; set; }
public required string Tag { get; set; }
/// <summary>When this version was the current text (i.e. when it was replaced).</summary>
public required DateTime EditedAt { get; set; }
}

View File

@@ -2,14 +2,36 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `channel_messages` table. One row per message.
///
/// Encryption: CipherText/Nonce/Tag use the channel AES key (ChannelDbKey), NOT any user's
/// RSA keypair. This means the server can decrypt for history queries; the per-recipient
/// RSA wrapping happens at delivery time in DeliverToServerMembers.
/// </summary>
public class ChannelMessages : Record
{
/// <summary>"channels:xyz" — which channel this belongs to.</summary>
public required string ChannelId { get; set; }
/// <summary>"users:keeper317" — who wrote it. Lowercased to match CoreClientService's id format.</summary>
public required string SenderUserId { get; set; }
/// <summary>Base64 AES-GCM ciphertext of the JSON-serialised ChatMessageContent.</summary>
public required string CipherText { get; set; }
/// <summary>Base64 AES-GCM 96-bit nonce. Different every message.</summary>
public required string Nonce { get; set; }
/// <summary>Base64 AES-GCM 128-bit authentication tag.</summary>
public required string Tag { get; set; }
/// <summary>UTC timestamp of original send. Drives history ordering.</summary>
public required DateTime CreatedAt { get; set; }
/// <summary>UTC timestamp of last edit. Null = never edited. Drives the (edited) bubble footer.</summary>
public DateTime? EditedAt { get; set; }
/// <summary>Soft-delete flag. Tombstones in history responses; bubbles show "deleted" placeholder.</summary>
public bool IsDeleted { get; set; }
}

View File

@@ -3,13 +3,38 @@ using RelayShared.Services;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `channels` table. One row per channel.
///
/// Lifecycle: created by HandleCreateChannel (or seeded by ServerBootstrapService at boot).
/// Soft-deleted by HandleDeleteChannel (IsDeleted flipped, row stays for audit).
/// </summary>
public class Channels : Record
{
/// <summary>Sidebar display name. Lowercased and dash-separated for new channels.</summary>
public required string Name { get; set; }
/// <summary>Creation timestamp. Drives sidebar sort order.</summary>
public required DateTime CreatedAt { get; set; }
/// <summary>Drives client rendering and server routing — Text/Voice/File/Forum/Stage.</summary>
public ChannelType Type { get; set; } = ChannelType.Text;
/// <summary>Sidebar category header (e.g. "General"). Empty means default group.</summary>
public string Group { get; set; } = string.Empty;
/// <summary>
/// True for announcement-style channels (#welcome, #files). Non-admins are blocked from
/// posting via PermissionService.CanSendMessagesAsync.
/// </summary>
public bool IsReadOnly { get; set; }
/// <summary>Soft-delete flag. Filtered out of channel-list builds in BuildChannelListForUser.</summary>
public bool IsDeleted { get; set; }
/// <summary>
/// Surreal record id of a File channel ("channels:xyz"). When set, ChatSocketBehavior's
/// MirrorAttachmentIfNeeded auto-copies non-gif attachments into the linked channel.
/// </summary>
public string? LinkedFileChannelId { get; set; }
}

View File

@@ -1,11 +1,26 @@
using SurrealDb.Net.Models;
using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `client_public_keys` table. Stores the RSA public key each user
/// has registered. Written by HandleRegisterKey, read by DeliverToServerMembers and history
/// fetches to encrypt outbound messages per recipient.
///
/// When a client reinstalls and regenerates a keypair, the existing row is updated rather
/// than duplicated (ClientKeyService.RegisterOrUpdateKeyAsync).
/// </summary>
public class ClientPublicKeys : Record
{
/// <summary>Mixed-case username as the user registered it. Used as the lookup key.</summary>
public required string Username { get; set; }
/// <summary>Base64 SubjectPublicKeyInfo (DER) of the user's RSA public key.</summary>
public required string PublicKey { get; set; }
/// <summary>When the user first registered.</summary>
public required DateTime CreatedAt { get; set; }
/// <summary>When the key was last updated (key rotation, reinstall).</summary>
public required DateTime UpdatedAt { get; set; }
}
}

View File

@@ -2,11 +2,28 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `server_encryption_keys` table. Stores both:
/// - The server's RSA keypair (for receiving encrypted client→server payloads).
/// - The single AES-256 key used to encrypt channel_messages at rest.
///
/// Generated once on first boot by ServerBootstrapService. Loaded into static fields on
/// ChatSocketBehavior at boot so handlers can use them without a DB round-trip.
/// </summary>
public class ServerEncryptionKeys : Record
{
/// <summary>Base64 AES-256 key used by ChannelCryptoService for at-rest message encryption.</summary>
public required string KeyBase64 { get; set; }
/// <summary>Base64 SubjectPublicKeyInfo of the server's RSA public key. Sent to clients on GetServerKey.</summary>
public required string PublicKey { get; set; }
/// <summary>Base64 PKCS8 of the server's RSA private key. Never leaves the server.</summary>
public required string PrivateKey { get; set; }
/// <summary>When the keys were generated.</summary>
public required DateTime CreatedAt { get; set; }
/// <summary>When the keys were last rotated. Currently same as CreatedAt — rotation isn't implemented.</summary>
public required DateTime UpdatedAt { get; set; }
}
}

View File

@@ -2,10 +2,24 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `channel_permissions` table. Per-(channel, role) override of a
/// role's base permissions.
///
/// Allow and Deny are independent masks (NOT a tri-state). Deny wins over Allow when both
/// have the same flag set. Bits not set in either fall through to the role's base permissions.
/// </summary>
public class ChannelPermissions : Record
{
/// <summary>"channels:xyz" — which channel this override applies in.</summary>
public required string ChannelId { get; set; }
/// <summary>"roles:abc" — which role this override applies to.</summary>
public required string RoleId { get; set; }
/// <summary>Permissions explicitly granted here (overrides "role doesn't have it" for this channel).</summary>
public PermissionFlags Allow { get; set; }
/// <summary>Permissions explicitly denied here. Wins over Allow.</summary>
public PermissionFlags Deny { get; set; }
}

View File

@@ -2,6 +2,18 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// The permission bitfield. The whole permission model is just:
///
/// ServerMembers.IsOwner = true → unconditional Administrator
/// roles.Permissions has Administrator flag → unconditional everything
/// channel_permissions.Deny has a specific flag → that permission denied here
/// channel_permissions.Allow has a specific flag → that permission allowed here
/// roles.Permissions has the flag → fallback (channel-independent)
///
/// PermissionService.HasPermissionAsync walks that ladder in order. See that class for the
/// authoritative implementation.
/// </summary>
[Flags]
public enum PermissionFlags
{
@@ -18,11 +30,21 @@ public enum PermissionFlags
DeleteChannel = 1 << 9 // Delete a channel
}
/// <summary>
/// Surreal record for the `roles` table. Defines a named permission bundle that can be
/// assigned to users via UserRoles.
/// </summary>
public class Roles : Record
{
/// <summary>Display name ("Admin", "Moderator", "Member").</summary>
public required string Name { get; set; }
/// <summary>Base permission bitfield. Channel-level overrides in ChannelPermissions can add or remove.</summary>
public required PermissionFlags Permissions { get; set; }
/// <summary>When the role was seeded.</summary>
public required DateTime CreatedAt { get; set; }
/// <summary>Tie-breaker for future multi-role-per-user scenarios. Lower = higher priority. Not used by the current ladder.</summary>
public int Priority { get; set; }
}

View File

@@ -2,9 +2,22 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `server_members` table. Membership list.
/// Drives DeliverToServerMembers (the fan-out target list for every chat message) and the
/// authoritative ownership flag for PermissionService.
/// </summary>
public class ServerMembers : Record
{
/// <summary>"users:keeper317" — references the Core users table by name convention.</summary>
public required string UserId { get; set; }
/// <summary>When the user was added to this server.</summary>
public required DateTime JoinedAt { get; set; }
/// <summary>
/// Authoritative owner flag. Owner gets unconditional Administrator via
/// PermissionService.IsServerOwnerAsync, independent of role assignments.
/// </summary>
public bool IsOwner { get; set; }
}
}

View File

@@ -2,9 +2,18 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `servers` table. Currently single-row (one server per deployment),
/// but the schema supports multi-server in the future.
/// </summary>
public class Servers : Record
{
/// <summary>Display name (currently "Test Server" from bootstrap).</summary>
public required string Name { get; set; }
/// <summary>"users:keeper317" — the owner. Mirrored as IsOwner=true on the matching ServerMembers row.</summary>
public required string OwnerUserId { get; set; }
/// <summary>Server creation timestamp.</summary>
public required DateTime CreatedAt { get; set; }
}
}

View File

@@ -2,9 +2,21 @@ using SurrealDb.Net.Models;
namespace RelayServer.Models;
/// <summary>
/// Surreal record for the `user_roles` table. Join table linking users to roles.
///
/// Invariant: ServerBootstrapService.SetUserRoleAsync guarantees exactly one row per user.
/// Multi-role-per-user isn't currently supported by the permission ladder — adding it would
/// just be a matter of removing the bootstrap's "delete stale rows" step.
/// </summary>
public class UserRoles : Record
{
/// <summary>"users:keeper317" — the assignee.</summary>
public required string UserId { get; set; }
/// <summary>"roles:abc" — the role being granted.</summary>
public required string RoleId { get; set; }
/// <summary>When the assignment was made.</summary>
public required DateTime AssignedAt { get; set; }
}