2026-09-15 17:54:16 +02:00
|
|
|
|
# Initial managed API contract
|
|
|
|
|
|
|
|
|
|
|
|
Status: initial port slice, API revision 1. No change to protobuf or media wire formats.
|
|
|
|
|
|
These are shared infrastructure APIs; the client-facing API follows with the client core.
|
|
|
|
|
|
|
|
|
|
|
|
## Protocol
|
|
|
|
|
|
|
|
|
|
|
|
`VoiceCat.Protocol` generates `Voicecat.V1` protobuf messages from the existing schema.
|
|
|
|
|
|
|
|
|
|
|
|
`ControlFraming.TryReadFrame(ref ReadOnlySequence<byte>, out ReadOnlySequence<byte>)`
|
|
|
|
|
|
extracts a payload and advances input only when a full frame exists. Returned memory
|
|
|
|
|
|
borrows the input's lifetime. Lengths above 16 MiB throw `InvalidDataException`.
|
|
|
|
|
|
Empty payloads are valid. `WriteFrame` and `WriteEnvelope` target `IBufferWriter<byte>`;
|
|
|
|
|
|
oversized outgoing payloads throw before output is written.
|
|
|
|
|
|
|
|
|
|
|
|
`ReadEnvelopesAsync(PipeReader, CancellationToken)` produces parsed envelopes and
|
|
|
|
|
|
advances consumed pipe data. It does not complete or dispose the caller's reader.
|
|
|
|
|
|
Clean EOF ends enumeration; partial EOF and oversized frames throw
|
|
|
|
|
|
`InvalidDataException`; malformed protobuf throws `InvalidProtocolBufferException`.
|
|
|
|
|
|
Cancellation propagates. A connection owner must close on protocol errors or
|
|
|
|
|
|
cancellation partway through a frame; partial frame bytes may already be consumed.
|
|
|
|
|
|
Fragments are consumed as they arrive so frames larger than pipe backpressure
|
|
|
|
|
|
thresholds make progress. Stopping enumeration between envelopes preserves the next frame.
|
|
|
|
|
|
|
|
|
|
|
|
`VoiceFrameHeader` is an immutable value with type, flags, codec, SSRC, sequence,
|
|
|
|
|
|
and timestamp. `Write(Span<byte>)` writes its 20-byte big-endian representation;
|
|
|
|
|
|
`TryRead` accepts at least 20 bytes and preserves unknown type/flag/codec values.
|
|
|
|
|
|
Higher layers decide which values they support.
|
|
|
|
|
|
|
|
|
|
|
|
## Media encryption
|
|
|
|
|
|
|
|
|
|
|
|
`MediaEncryptor` and `MediaDecryptor` each own one directional 32-byte session key
|
|
|
|
|
|
and mutable packet state. Use one owner at a time; they provide no synchronization.
|
2026-09-15 18:04:20 +02:00
|
|
|
|
Production constructs them through `TlsSession` media factories after its handshake.
|
|
|
|
|
|
Raw-key constructors support conformance tests.
|
2026-09-15 17:54:16 +02:00
|
|
|
|
|
|
|
|
|
|
`MediaEncryptor.Encrypt(VoiceFrameHeader, ReadOnlySpan<byte>, Span<byte>)` writes
|
|
|
|
|
|
the full header plus ciphertext and 16-byte tag and returns packet length. It replaces
|
|
|
|
|
|
the supplied sequence with its own counter, starting at zero. Capacity and overlap
|
|
|
|
|
|
errors throw before reserving a counter. Reserved counters are never reused after
|
|
|
|
|
|
encryption failure. At `ulong.MaxValue`, encryption throws and requires a new session.
|
|
|
|
|
|
|
|
|
|
|
|
`MediaDecryptor.TryDecrypt(ReadOnlySpan<byte>, Span<byte>, out VoiceFrameHeader,
|
|
|
|
|
|
out int)` authenticates and decrypts a complete packet. Short packets, failed tags,
|
|
|
|
|
|
replays, and packets outside the 64-packet window return false with default header
|
|
|
|
|
|
and zero bytes written. Authentication failure clears the attempted plaintext region;
|
|
|
|
|
|
structural/replay rejection leaves storage untouched. Callers must only consume
|
|
|
|
|
|
output after success. Invalid storage capacity and overlapping buffers throw.
|
|
|
|
|
|
|
|
|
|
|
|
The nonce is four zero bytes plus the big-endian header counter. All 20 header bytes
|
|
|
|
|
|
are authenticated associated data. The replay window advances after authentication.
|
|
|
|
|
|
The platform ChaCha20-Poly1305 implementation is preferred; BouncyCastle is used when
|
|
|
|
|
|
platform support is absent. Both produce the same wire bytes. The fallback currently
|
|
|
|
|
|
allocates per packet; audio and relay allocation guarantees are later checkpoints.
|
|
|
|
|
|
|
|
|
|
|
|
Dispose both objects to clear their owned key arrays and release platform crypto
|
|
|
|
|
|
resources. Use after disposal throws `ObjectDisposedException`.
|
2026-09-15 18:04:20 +02:00
|
|
|
|
|
|
|
|
|
|
## TLS sessions
|
|
|
|
|
|
|
|
|
|
|
|
`TlsSession` is a single-owner, nonblocking BouncyCastle TLS 1.3 state machine.
|
|
|
|
|
|
It owns no socket or worker thread. The transport owner feeds `ReceiveCiphertext`,
|
|
|
|
|
|
fully drains `DrainCiphertext` to its socket (including partial sends), and reads
|
|
|
|
|
|
application data through `ReadPlaintext`. Reads and drains return a byte count and
|
|
|
|
|
|
may require repeated calls. `WritePlaintext` requires `IsReady`. Socket cancellation,
|
|
|
|
|
|
backpressure, and connection lifetime belong to the transport owner.
|
|
|
|
|
|
|
|
|
|
|
|
`CreateClient(Func<string, bool>)` requires an explicit certificate acceptance
|
|
|
|
|
|
callback. It receives the uppercase SHA-256 fingerprint of the leaf certificate's
|
|
|
|
|
|
DER bytes during the handshake. Returning false rejects the session before application
|
|
|
|
|
|
data or media keys are available. This is TOFU certificate pinning; there is no PKI
|
|
|
|
|
|
chain or hostname validation. The synchronous callback must have the trust decision
|
|
|
|
|
|
available; an asynchronous first-connect prompt requires a subsequent connection
|
|
|
|
|
|
after explicit acceptance. Never automatically accept or persist an unknown pin.
|
|
|
|
|
|
|
|
|
|
|
|
`CreateServer(certificatePem, privateKeyPem)` supports ECDSA credentials; use
|
|
|
|
|
|
`ServerCredentials.CreateTlsSession()` to import persisted credentials. TLS 1.2 is
|
|
|
|
|
|
rejected. Handshake completion captures two 32-byte exporter keys using label
|
|
|
|
|
|
`voicecat media v1` and one-byte contexts 0 (client to server) and 1 (server to client).
|
|
|
|
|
|
BouncyCastle discards its exporter secrets after that callback. Media factories
|
|
|
|
|
|
select the correct direction for each role and require a ready session.
|
|
|
|
|
|
|
|
|
|
|
|
Create one encryptor and decryptor per connection and retain them for the connection's
|
|
|
|
|
|
lifetime: constructing a second encryptor resets its counter and would reuse nonces.
|
|
|
|
|
|
Dispose media objects separately from the TLS session. `Close()` queues close_notify;
|
|
|
|
|
|
drain it before disposal. On socket EOF call `CompleteInput()`; missing close_notify
|
|
|
|
|
|
throws `IOException`. TLS/protocol errors require closing the connection. Disposal
|
|
|
|
|
|
clears the session's owned exporter arrays and scratch buffer.
|
|
|
|
|
|
|
|
|
|
|
|
## Persisted trust and credentials
|
|
|
|
|
|
|
|
|
|
|
|
`TofuStore` uses the existing UTF-8 `host:port lowercase-hex-fingerprint` format.
|
|
|
|
|
|
Host matching is ordinal and case sensitive, matching native behavior. `Check`
|
|
|
|
|
|
returns `FirstConnect`, `Matched`, or `Mismatch` without changing persistence.
|
|
|
|
|
|
Only explicit `Pin` or `Remove` changes the file. Pin replacement requires an
|
|
|
|
|
|
explicit caller decision; malformed files fail closed. Changes replace the file
|
|
|
|
|
|
atomically before updating memory. Use one owner per store/file.
|
|
|
|
|
|
|
2026-09-16 22:13:34 +02:00
|
|
|
|
`ServerProfile` is the app-facing saved-server model shared by platform clients. Construct
|
|
|
|
|
|
profiles with `ServerProfile.Create`, which trims values and requires a host, nonzero port,
|
|
|
|
|
|
and username for account authentication. A profile contains only its stable ID, endpoint,
|
|
|
|
|
|
authentication mode, username or guest nickname. It deliberately has no password field.
|
|
|
|
|
|
Platform clients own any secret storage, such as macOS Keychain integration.
|
|
|
|
|
|
|
|
|
|
|
|
`ServerProfileStore` serializes profiles as camel-case JSON with string authentication
|
|
|
|
|
|
values. Missing, malformed and unreadable files load as an empty list; invalid individual
|
|
|
|
|
|
profiles are discarded. Saving filters invalid entries and replaces the file through a
|
|
|
|
|
|
same-directory temporary file. Use one owner per store/file and do not treat it as a
|
|
|
|
|
|
credential store.
|
|
|
|
|
|
|
2026-09-15 18:04:20 +02:00
|
|
|
|
`ServerIdentity` reads and writes the native 96-byte Ed25519 format:
|
|
|
|
|
|
`public-key[32] || seed[32] || public-key[32]`. Loading verifies both public-key
|
|
|
|
|
|
copies against the seed. Disposal clears the owned seed.
|
|
|
|
|
|
|
|
|
|
|
|
`ServerCredentials.LoadOrCreate(directory, serverName)` imports `identity.key`,
|
|
|
|
|
|
`server.crt`, and `server.key` unchanged. If all are absent it creates an ECDSA-P256
|
|
|
|
|
|
self-signed certificate and identity. If only some exist it rejects startup rather
|
|
|
|
|
|
than rotating identity. Restore the missing files. New certificates include SAN URI
|
|
|
|
|
|
`urn:voicecat:identity:ed25519:<lowercase-public-key-hex>`; legacy certificates are
|
|
|
|
|
|
accepted unchanged. Checking this URI against ServerHello's identity is deferred
|
|
|
|
|
|
until the managed handshake/session layer is implemented; trust currently pins the
|
|
|
|
|
|
leaf certificate. Dispose credentials after their TLS sessions are created/finished
|
|
|
|
|
|
as required by the application lifetime.
|
|
|
|
|
|
|
|
|
|
|
|
Private file writes use a same-directory temporary file, flush, and atomic replacement.
|
|
|
|
|
|
On Unix new files use owner read/write permissions; Windows inherits directory ACLs.
|
|
|
|
|
|
The credential directory must have one provisioning owner. PEM strings and crypto
|
|
|
|
|
|
library internal copies are managed memory; owned-array clearing does not promise
|
|
|
|
|
|
erasure of every runtime/library copy.
|
2026-09-15 22:51:33 +02:00
|
|
|
|
|
|
|
|
|
|
## Codec and DSP
|
|
|
|
|
|
|
|
|
|
|
|
`VoiceCat.Codec` and `VoiceCat.Dsp` call the desktop `voicecat_media` native library
|
|
|
|
|
|
through source-generated `LibraryImport`. It links pinned Opus 1.5.2 and the existing
|
|
|
|
|
|
vendored RNNoise; it has no dependency on libvoicecat or its C ABI. Fixed C signatures
|
|
|
|
|
|
wrap Opus controls so P/Invoke never calls C varargs. SafeHandle owns every native
|
|
|
|
|
|
encoder, decoder, DRED parser/state, and denoiser, including failed initialization.
|
|
|
|
|
|
|
|
|
|
|
|
`OpusOptions` is an immutable record. Supported PCM rates are 8/12/16/24/48 kHz,
|
2026-09-16 16:48:06 +02:00
|
|
|
|
one or two interleaved channels, and integral 5/10/20/40/60 ms frames. These match the
|
2026-09-15 22:51:33 +02:00
|
|
|
|
current VoiceCat protocol's integer frame duration; fractional Opus frame durations
|
|
|
|
|
|
are not exposed. Low-delay application mode requires at most 20 ms. Channel capture
|
|
|
|
|
|
bandwidth is controlled separately by `MaximumBandwidthHz`; the production audio
|
|
|
|
|
|
clock will remain 48 kHz. Options are validated before native creation, and native
|
|
|
|
|
|
control failures throw `OpusException` with the libopus error code.
|
|
|
|
|
|
|
|
|
|
|
|
`OpusEncoder.Encode(ReadOnlySpan<short>, Span<byte>)` accepts exactly one frame
|
|
|
|
|
|
and returns encoded bytes. `OpusDecoder.Decode(packet, pcm, samplesPerChannel,
|
|
|
|
|
|
recoverPreviousFrame)` returns samples **per channel**, not total interleaved samples.
|
|
|
|
|
|
An empty packet requests PLC. Passing the next packet with `recoverPreviousFrame`
|
|
|
|
|
|
requests in-band FEC; absence of FEC permits libopus's PLC fallback. Decode that next
|
|
|
|
|
|
packet normally afterward. Capacity/overlap errors throw before native processing.
|
|
|
|
|
|
|
|
|
|
|
|
DRED is explicit. Unsupported native builds reject `DeepRedundancy = true` rather
|
|
|
|
|
|
than silently disabling it. With pinned Opus 1.5.2, DRED encoding requires PCM at
|
|
|
|
|
|
16/24/48 kHz; its activity analysis cannot emit DRED at 8/12 kHz. Such configurations
|
|
|
|
|
|
are rejected. DRED packets can still be decoded at all five rates. The encoder uses
|
|
|
|
|
|
a 30 ms minimum redundancy duration because this release needs two redundancy chunks;
|
|
|
|
|
|
the old 20 ms setting produces no DRED packets. Actual redundancy remains adaptive
|
|
|
|
|
|
to bitrate, loss estimate, and activity; it is not guaranteed in every packet.
|
|
|
|
|
|
|
|
|
|
|
|
`OpusDeepRedundancy.TryRecover(audioDecoder, nextPacket, pcm, samplesPerChannel,
|
|
|
|
|
|
offset)` parses the next packet and reconstructs a missing frame. Default offset is
|
|
|
|
|
|
one missing frame's samples per channel before the next packet's start, matching
|
|
|
|
|
|
libopus's offset convention. A packet without DRED returns false; then the owner
|
|
|
|
|
|
can try FEC/PLC. Only consume recovery output on success. Parse/native errors throw.
|
|
|
|
|
|
|
|
|
|
|
|
`RnnoiseProcessor.Process(Span<short>, sampleRate)` operates in place on complete
|
|
|
|
|
|
480-sample mono chunks at 48 kHz. Other rates pass through unchanged; partial chunks
|
|
|
|
|
|
at 48 kHz throw instead of leaving a tail silently untreated. Float scratch is
|
|
|
|
|
|
preallocated, and rounding/clipping matches the C++ processor. Use distinct instances
|
|
|
|
|
|
for stereo channels when the later pipeline supports stereo microphone denoising.
|
|
|
|
|
|
Noise reduction does not gate speech.
|
|
|
|
|
|
|
|
|
|
|
|
`EnergyVadProcessor.Process(ReadOnlySpan<short>)` compares normalized RMS against
|
|
|
|
|
|
`Threshold`, retains speech for `HangTime`, and starts closed. It uses monotonic
|
|
|
|
|
|
`TimeProvider` timestamps; tests inject a clock. Threshold changes are atomic; all
|
|
|
|
|
|
processing state otherwise has one owner. Codec/DSP processing methods allocate no
|
|
|
|
|
|
managed memory after initialization, verified across 1,000 combined cycles. They run
|
|
|
|
|
|
on a managed worker, never the native real-time device callback. Native device rings,
|
|
|
|
|
|
jitter, mixer, and audio scheduling remain later work.
|
|
|
|
|
|
|
|
|
|
|
|
## Initial managed server
|
|
|
|
|
|
|
|
|
|
|
|
`VoiceServer(directory, endpoint, allowGuests, name)` owns credentials, the SQLite
|
|
|
|
|
|
store, a TCP listener and its connection tasks. `EndPoint` reports the actual bound
|
|
|
|
|
|
port (zero requests an ephemeral port). Dispose asynchronously to stop the listener
|
|
|
|
|
|
and wait for all connections. The CLI currently binds loopback and accepts optional
|
|
|
|
|
|
data-directory/port positional arguments.
|
|
|
|
|
|
|
|
|
|
|
|
All control traffic uses TLS 1.3 with the existing v2 protobuf. A single async loop
|
|
|
|
|
|
owns each `TlsSession`; handlers exchange envelopes through bounded queues.
|
|
|
|
|
|
This checkpoint caps connections at 64, queued input at 32 envelopes, queued output
|
|
|
|
|
|
at 64 envelopes, and each control payload at 64 KiB (stricter than the shared framer's
|
|
|
|
|
|
16 MiB limit). Queue exhaustion disconnects slow consumers. Handshake timeout is
|
2026-09-15 22:58:16 +02:00
|
|
|
|
15 seconds by default. Completed TLS connections use the server's media-aware reaper.
|
|
|
|
|
|
|
|
|
|
|
|
The existing `VoiceServer(directory, endpoint, allowGuests, name)` constructor remains
|
|
|
|
|
|
available. An overload accepts `VoiceServerOptions` and an optional `TimeProvider`.
|
|
|
|
|
|
Options configure server name, guest access, connection limit (default 64), handshake
|
|
|
|
|
|
timeout (15 seconds), idle timeout (45 seconds) and reaper interval (15 seconds).
|
|
|
|
|
|
Zero idle timeout disables reaping; active reaping requires a positive interval.
|
|
|
|
|
|
Invalid options fail before creating credentials, databases or sockets.
|
2026-09-15 22:51:33 +02:00
|
|
|
|
|
2026-09-15 23:24:11 +02:00
|
|
|
|
Options also configure authentication burst/refill (5 attempts / one per ten seconds).
|
|
|
|
|
|
The bounded address/account limiter runs before Argon2 and survives reconnects within
|
|
|
|
|
|
the process, with escalating failure backoff. `Completion` reports unexpected termination
|
|
|
|
|
|
of listener/media/active-reaper tasks; hosts should observe it and stop on failure.
|
|
|
|
|
|
The executable supports configuration/environment, local account provisioning, JSON
|
|
|
|
|
|
readiness, exclusive instance locking and bounded graceful shutdown; see deployment.md.
|
|
|
|
|
|
|
2026-09-15 22:51:33 +02:00
|
|
|
|
Authentication starts users in unprotected Lobby (id 1), subject to its capacity.
|
|
|
|
|
|
Success returns permissions, then a cloned snapshot; peers receive joined/updated/left
|
|
|
|
|
|
events. Server-authoritative text replaces supplied sender ids/timestamps, limits
|
|
|
|
|
|
bodies to 4096 UTF-8 bytes, and acknowledges valid or rejected routing. Channel text
|
2026-09-15 23:11:09 +02:00
|
|
|
|
requires membership; private text echoes to sender and recipient. Protected joins enforce
|
|
|
|
|
|
the supplied password and capacity; `LeaveChannel` returns to Lobby. Passwords use the
|
|
|
|
|
|
native salted, keyed BLAKE2b-256 `salt_hex:hash_hex` format, verified in both directions.
|
|
|
|
|
|
|
|
|
|
|
|
Channel create/edit/delete persist before broadcasting events. Administrators can manage
|
|
|
|
|
|
all channels; `CanCreateTempChannel` permits creation of temporary channels only. Edit with
|
|
|
|
|
|
an empty password preserves the existing hash, matching native behavior; password removal
|
|
|
|
|
|
has no v2 request representation. Lobby cannot be deleted, protected or nested. Missing
|
|
|
|
|
|
parents, tree cycles and deletion of parents with children fail without mutation. Deletion
|
|
|
|
|
|
moves members to Lobby (even if full), clearing their streams. Edits stop existing streams
|
|
|
|
|
|
so clients must negotiate the updated audio configuration. Channel names/topics/passwords
|
|
|
|
|
|
are limited to 128/4096/1024 UTF-8 bytes. Audio requires Opus, 48 kHz, mono/stereo,
|
|
|
|
|
|
500–512000 bps, integral 5/10/20/40/60 ms frames and valid application/loss/complexity.
|
|
|
|
|
|
Database v2 has no DRED column; CRUD rejects DRED rather than silently losing it on restart.
|
|
|
|
|
|
|
|
|
|
|
|
Session permissions gate kick/ban/move/mute and account operations. Only administrators
|
|
|
|
|
|
can grant permissions; account-administration permission cannot grant administrator status.
|
|
|
|
|
|
These two permission restrictions are stricter than the C++ oracle. Moves bypass channel
|
|
|
|
|
|
passwords but respect capacity and clear streams. Server mute/deafen immediately updates
|
|
|
|
|
|
encrypted routing. Kick/ban retire routing before closure and emit one LEFT with the reason.
|
|
|
|
|
|
Account bans persist by username; guest bans persist by address because nicknames are not
|
|
|
|
|
|
identities. Ban wire expiry is Unix milliseconds, converted to database seconds rounded up;
|
|
|
|
|
|
zero means permanent. This fixes the native handler's millisecond/second mismatch.
|
|
|
|
|
|
Existing sessions on the same address/account are not swept by a target-user ban.
|
|
|
|
|
|
|
|
|
|
|
|
Create/reset/delete/list accounts require administrator or `CanAdminAccounts`. New accounts
|
|
|
|
|
|
are non-admin. Bounded Argon2 work runs outside the server state lock; authority is checked
|
|
|
|
|
|
when accepting the operation, and cancellation is checked before password writes. Reset
|
|
|
|
|
|
and deletion affect future authentication; existing sessions retain their permissions.
|
|
|
|
|
|
Lists omit password hashes and return millisecond timestamps. Oversized lists fail instead
|
|
|
|
|
|
of truncating or exceeding the 64 KiB frame limit. Privileged responses echo request ids;
|
|
|
|
|
|
generic codes are 6 for permission denied and 3 for invalid/missing/duplicate input.
|
2026-09-15 22:53:54 +02:00
|
|
|
|
|
|
|
|
|
|
`VoiceServer.MediaEndPoint` exposes the bound UDP endpoint; UDP uses the same address
|
|
|
|
|
|
and port number as TCP, and `ServerHello.udp_port` advertises it. Successful authentication
|
|
|
|
|
|
issues a 16-byte binding token. TLS confirmation echoes an acknowledgement; a protocol-v2
|
|
|
|
|
|
bootstrap packet binds the first UDP endpoint. Tokens cannot replace an established
|
|
|
|
|
|
endpoint; reconnect to change endpoints. Invalid tokens and malformed packets are ignored.
|
|
|
|
|
|
|
|
|
|
|
|
Voice subscription, unsubscribe, stream announce/stop and stream-state signaling are
|
|
|
|
|
|
implemented. Announces require subscription and support microphone, screen audio and
|
|
|
|
|
|
auxiliary device streams, with at most 16 streams per user and labels up to 128 characters.
|
|
|
|
|
|
Stream ids are monotonically assigned per user; SSRCs are assigned server-wide.
|
|
|
|
|
|
Channel audio settings are authoritative; requested bitrate may lower the channel ceiling
|
|
|
|
|
|
(nonzero requests below 500 bps fail). User updates include the actor. Stream-state updates
|
|
|
|
|
|
use the authenticated sender id and ignore unknown stream ids.
|
|
|
|
|
|
|
|
|
|
|
|
Channel movement clears active streams; joining the current channel preserves them.
|
|
|
|
|
|
Unsubscribe clears streams. Disconnect removes routing and retires media resources,
|
|
|
|
|
|
even if no UDP traffic follows. Senders must own the SSRC and be subscribed; recipients
|
|
|
|
|
|
must be subscribed, bound, in the same channel and not deafened. Server-muted senders
|
|
|
|
|
|
cannot relay. Every voice packet is authenticated with the sender's directional key;
|
|
|
|
|
|
the SFU reseals encoded bytes for each recipient without decoding, replacing only the
|
|
|
|
|
|
sequence and ciphertext/tag. Replay rejection precedes authentication; successful
|
|
|
|
|
|
authentication advances the replay window.
|
|
|
|
|
|
|
|
|
|
|
|
The UDP loop exclusively owns media crypto, endpoint mutation and packet buffers.
|
|
|
|
|
|
Control handlers publish immutable routing snapshots. Crypto is created within the
|
|
|
|
|
|
TLS owner loop and transferred once. A coalesced notification wakes retired-key cleanup.
|
|
|
|
|
|
The synchronous fan-out core allocates zero managed bytes with platform ChaCha20-Poly1305;
|
|
|
|
|
|
socket scheduling and the allocating BouncyCastle fallback are outside that guarantee.
|
2026-09-15 22:58:16 +02:00
|
|
|
|
UDP keepalives are echoed for bound endpoints. Any parsed control envelope, authenticated
|
|
|
|
|
|
voice from an active owned stream, or exact header-only keepalive from a bound endpoint
|
|
|
|
|
|
refreshes a shared monotonic activity timestamp. Invalid media does not refresh it.
|
|
|
|
|
|
The reaper sends a fatal disconnect, removes presence/routing and broadcasts one LEFT
|
|
|
|
|
|
event. Valid UDP activity keeps a TCP-idle client alive. Shutdown cancels and awaits
|
|
|
|
|
|
the accept, reaper, control and media loops before disposing credentials/storage.
|
2026-09-15 22:51:33 +02:00
|
|
|
|
|
|
|
|
|
|
`AccountStore(path)` retains the C++ schema version 2, accepts version 1 migration,
|
|
|
|
|
|
and rejects unknown revisions. Opening an existing channel table does not reseed it.
|
|
|
|
|
|
Account creation/authentication uses parameterized SQL; two password workers bound
|
|
|
|
|
|
per-store Argon2 work. Failed authentication leaves `last_login` unchanged. Dispose
|
2026-09-15 23:11:09 +02:00
|
|
|
|
after its operations finish. `ResetPasswordAsync`, `DeleteAccount` and `ListAccounts`
|
|
|
|
|
|
also expose administration to hosts. Initial administrator provisioning uses this API or
|
|
|
|
|
|
the native administration CLI; there is no automatic bootstrap account.
|
2026-09-15 22:51:33 +02:00
|
|
|
|
|
|
|
|
|
|
`PasswordHasher` uses strict UTF-8 without normalization and libsodium-compatible
|
|
|
|
|
|
Argon2id v19 PHC strings: 16-byte salt, 32-byte output, new-hash parameters
|
|
|
|
|
|
64 MiB memory, two iterations, parallelism one. Verification supports up to 128 MiB,
|
|
|
|
|
|
ten iterations, parallelism four and 1024 UTF-8 password bytes; malformed or excessive
|
|
|
|
|
|
hashes fail closed. Standard C++ interactive-cost accounts are preserved. These
|
|
|
|
|
|
bounds intentionally reject imported hashes above those costs. Native fixtures cover
|
|
|
|
|
|
ASCII, Unicode and embedded NUL; the database oracle verifies cross-implementation
|
|
|
|
|
|
authentication in both directions.
|
|
|
|
|
|
|
|
|
|
|
|
SQLite's MIT provider/bundle uses the pinned public-domain SourceGear SQLite build.
|
|
|
|
|
|
The license audit checks that exact package version and repository identity because
|
|
|
|
|
|
the native package lacks a NuGet license expression; other dependencies still require
|
|
|
|
|
|
an approved permissive expression.
|