# 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, out ReadOnlySequence)` 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`; 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)` 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. Production constructs them through `TlsSession` media factories after its handshake. Raw-key constructors support conformance tests. `MediaEncryptor.Encrypt(VoiceFrameHeader, ReadOnlySpan, Span)` 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, Span, 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`. ## 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)` 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. `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. `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:`; 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. ## 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, one or two interleaved channels, and integral 5/10/20/40/60 ms frames. These match the 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, Span)` 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, 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)` 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 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. 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. 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 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. `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. 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. `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 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. `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.