dave

    Safe native MoonBit bindings for Discord's official libdave C API

    dave
    libdave
    discord
    e2ee
    mls
    native
    ffi
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    17 days ago
    Downloads
    40

    #gaato/dave

    Safe native MoonBit bindings for Discord's official libdave C API.

    gaato/dave owns the unsafe FFI boundary and exposes opaque MoonBit types for MLS sessions, key ratchets, and media encryptors/decryptors. It is a protocol and media-crypto library, not a Discord client: Gateway and voice WebSocket opcodes, participant bookkeeping, transition timing, RTP/UDP transport, and recovery policy stay in the application.

    Real DAVE operations target MoonBit's native backend and pin upstream libdave v1.2.0/cpp. The package also type-checks on JavaScript so it can remain in a multi-target dependency graph; there available() is false and native-handle constructors raise DaveError::LibraryUnavailable.

    #Using the API

    Add the released module to the consumer and import the root package from moon.pkg:

    moon add gaato/dave@0.1.0

    import {
    "gaato/dave",
    }

    Create an MLS session and obtain the key package that the Discord DAVE orchestration layer sends to the voice server:

    ///|
    fn make_key_package() -> Bytes raise @dave.DaveError {
    let session = @dave.Session::new(
    protocol_version=1,
    group_id=42UL,
    self_user_id=100UL,
    )
    session.key_package()
    }

    The application sets the external sender before processing proposals, then feeds proposal, commit, and Welcome messages into the session. The external sender cannot be replaced while pending or established MLS group state exists; call reset, replace it, and then reinitialize when a replacement is needed. reset itself retains the configured external sender, matching libdave, so an ordinary reinitialize recreates pending group state with that sender. Once an epoch is established, obtain per-user KeyRatchet values and install them into an Encryptor or Decryptor. Outbound SSRCs must be assigned a supported codec before encrypted frames are processed.

    Check available() before starting an optional DAVE path, or let constructors raise DaveError::LibraryUnavailable when DAVE is required.

    #Ownership and errors

    • Session, KeyRatchet, Encryptor, and Decryptor own finalizable native handles. Copying one of these MoonBit values aliases the same mutable native state, so access to a single value must be serialized by the application.
    • Byte strings returned from native code are copied into MoonBit-owned memory. Installing a KeyRatchet copies its native key state into the media object.
    • Session::new always requests libdave's transient signing-key behavior in v0.1. Native authentication-session IDs and persisted-key lookup are not exposed by the safe API.
    • Invalid use, unavailable native support, and wrapper/native failures raise DaveError. MLS commit and Welcome rejections remain protocol outcomes in CommitResult::Failed or WelcomeResult::Failed. Applied commit and Welcome results both carry roster deltas; RosterChange::Remove represents an empty signature returned by libdave.

    #Verification boundary

    The official libdave v1.2.0 prebuilt C ABI has PERSISTENT_KEYS disabled. Each Session therefore has a different transient signing identity, even for the same Discord user, and Session::reinitialize rotates that identity again. Concurrent sessions must not be treated as sharing a persistent DAVE identity.

    Session::current_group_pairwise_fingerprint_blocking compares signing keys in the currently established MLS group only. It can produce the current group's DAVE verification code, but it does not provide identity continuity, a durable verified-contact state, or persistent verification across reconnects, reinitialization, or a replacement Session. Applications must discard any stored fingerprint, display code, or trust decision when that boundary changes. The fingerprint-format version is fixed to the canonical protocol value zero; callers cannot substitute another version. The method waits for libdave's asynchronous fingerprint worker and must not be called on an event-loop thread.

    Formatting the current-group result is an explicit second step:

    let fingerprint = session.current_group_pairwise_fingerprint_blocking(
    user_id=peer_user_id,
    )
    let display_code = @dave.pairwise_verification_code(fingerprint)

    #Native runtime

    Opt in once from a native Moon build to download the matching official libdave archive, verify its pinned size and SHA-256 digest, and store the complete extraction in a user cache:

    env MBT_DAVE_REQUIRE_NATIVE=1 moon build --target native --release --deny-warn

    The shared library is loaded at runtime; it is not installed system-wide or embedded in the Mooncake. Builds without MBT_DAVE_REQUIRE_NATIVE=1, including JavaScript builds, do not bootstrap a host library.

    See Native libdave runtime for cache paths, offline and preseeded builds, environment overrides, and loader order.

    The upstream release provides these host assets:

    HostArchitectureCI coverage
    Linuxx86-64native build and tests
    LinuxARM64native build and tests
    macOSARM64native build and tests
    macOSx86-64asset bootstrap and digest smoke test
    Windowsx86-64native build and tests

    The official MoonBit installer currently has no Darwin x86-64 toolchain, so the macOS Intel job cannot compile or execute the binding. Windows ARM64 and other hosts have no pinned upstream libdave asset. Host-selected bootstrap does not support cross-compilation.

    Upstream's Linux binaries require glibc 2.38 and GLIBCXX 3.4.32. Applications on older distributions need a compatible self-built library supplied through MBT_DAVE_NATIVE_LIB or a newer runtime environment.

    #Verification

    On a supported host, bootstrap the pinned runtime and verify both backends with:

    moon fmt --check node --test test/build.test.js moon check --target all --deny-warn env MBT_DAVE_REQUIRE_NATIVE=1 moon build --target native --release --deny-warn env MBT_DAVE_REQUIRE_NATIVE=1 moon test --target native --release --deny-warn moon test --target js --release --deny-warn moon info --target native moon package --list

    The offline Node downloader tests under test/ are repository-only and are excluded from the published Mooncake.

    The pinned native bridge smoke exercises the safe MoonBit/C boundary through a successful single-member commit, roster and authenticator decoding, key-ratchet installation, and encrypted media round trip. It does not cover a server-signed Add proposal, Welcome processing, or multi-party Discord DAVE conformance. Those paths require libdave's upstream test-only ExternalSender helper or messages produced by a live Discord voice session.

    #Upstream and licensing

    This project is an independent binding. It is not affiliated with or endorsed by Discord. libdave and its bundled dependencies retain their upstream licenses and notices; see THIRD_PARTY.md. The MoonBit binding itself is licensed under Apache-2.0.

    #Checked API examples

    The documentation examples below are compile-checked without requiring a live Discord session.

    ///|
    fn make_key_package_for_docs() -> Bytes raise @dave.DaveError {
    let session = @dave.Session::new(
    protocol_version=1,
    group_id=42UL,
    self_user_id=100UL,
    )
    session.key_package()
    }

    ///|
    fn current_group_verification_code_for_docs(
    session : @dave.Session,
    peer_user_id : UInt64,
    ) -> String raise @dave.DaveError {
    let fingerprint = session.current_group_pairwise_fingerprint_blocking(
    user_id=peer_user_id,
    )
    @dave.pairwise_verification_code(fingerprint)
    }

    ///|
    test "documented native flows typecheck" {
    let _ = make_key_package_for_docs
    let _ = current_group_verification_code_for_docs
    }

    DaveError

    pub(all) suberror DaveError {
    LibraryUnavailable(reason~ : String)
    InvalidArgument(operation~ : String, reason~ : String)
    InvalidState(operation~ : String, reason~ : String)
    MlsOperationFailed(operation~ : String, failure~ : MlsFailure)
    FingerprintFailed(user_id~ : UInt64, reason~ : String)
    EncryptFailed(reason~ : EncryptFailure)
    DecryptFailed(reason~ : DecryptFailure)
    NativeFailure(operation~ : String, reason~ : String)
    } derive(Eq,
    Debug
    )

    Errors reported by the safe DAVE wrapper.

    Protocol-level commit and Welcome rejection is returned as CommitResult::Failed or WelcomeResult::Failed, rather than raised as an error. Raised errors represent invalid wrapper use, unavailable native support, or an operation that could not produce a protocol result.

    Codec

    pub(all) enum Codec {
    Opus
    Vp8
    Vp9
    H264
    H265
    Av1
    } derive(Eq,
    Debug
    )

    A codec supported by libdave's frame processors.

    libdave's Unknown codec is deliberately omitted because it cannot safely be used for encryption.

    CommitResult

    pub(all) enum CommitResult {
    Applied(changes~ : Array[RosterChange])
    Ignored
    Failed(failure~ : MlsFailure)
    } derive(Eq,
    Debug
    )

    The protocol outcome of processing an MLS commit.

    DecryptFailure

    pub(all) enum DecryptFailure {
    DecryptionFailure
    MissingKeyRatchet
    InvalidNonce
    MissingCryptor
    Unknown(code~ : Int)
    } derive(Eq,
    Debug
    )

    Why media-frame decryption failed.

    Decryptor

    pub struct Decryptor {
    // private fields
    }

    A stateful media-frame decryptor.

    Applications normally keep one decryptor per remote participant. The native handle is released by a finalizer; copied Decryptor values alias the same mutable state and must not be used concurrently.

    Decryptor::decrypt

    fn Decryptor::decrypt(self : Decryptor, media_type~ : MediaType, encrypted_frame : Bytes) -> Bytes raise DaveError

    Decrypt one media frame and return a new MoonBit-owned byte string.

    Decryptor::new

    fn Decryptor::new() -> Decryptor raise DaveError

    Create a media-frame decryptor.

    Decryptor::transition_to_key_ratchet

    fn Decryptor::transition_to_key_ratchet(self : Decryptor, key_ratchet : KeyRatchet) -> Unit raise DaveError

    Begin libdave's transition to a copy of key_ratchet.

    libdave retains old decryption keys for its built-in transition window.

    Decryptor::transition_to_passthrough_mode

    fn Decryptor::transition_to_passthrough_mode(self : Decryptor, enabled~ : Bool) -> Unit raise DaveError

    Transition to or from unencrypted passthrough.

    EncryptFailure

    pub(all) enum EncryptFailure {
    EncryptionFailure
    MissingKeyRatchet
    MissingCryptor
    TooManyAttempts
    Unknown(code~ : Int)
    } derive(Eq,
    Debug
    )

    Why media-frame encryption failed.

    Encryptor

    pub struct Encryptor {
    // private fields
    }

    A stateful media-frame encryptor.

    Assign each SSRC to its codec before encrypting frames for that SSRC. The native handle is released by a finalizer; copied Encryptor values alias the same mutable state and must not be used concurrently.

    Encryptor::assign_ssrc_to_codec

    fn Encryptor::assign_ssrc_to_codec(self : Encryptor, ssrc~ : UInt, codec~ : Codec) -> Unit raise DaveError

    Associate an RTP SSRC with its media codec.

    Encryptor::encrypt

    fn Encryptor::encrypt(self : Encryptor, media_type~ : MediaType, ssrc~ : UInt, frame : Bytes) -> Bytes raise DaveError

    Encrypt one media frame and return a new MoonBit-owned byte string.

    SSRC/codec assignment is required while encryption is active. Native passthrough mode accepts frames without an assignment, matching libdave.

    Encryptor::has_key_ratchet

    fn Encryptor::has_key_ratchet(self : Encryptor) -> Bool

    Whether an outbound key ratchet has been installed.

    Encryptor::is_passthrough_mode

    fn Encryptor::is_passthrough_mode(self : Encryptor) -> Bool

    Whether outbound media is currently passed through unencrypted.

    Encryptor::new

    fn Encryptor::new() -> Encryptor raise DaveError

    Create a media-frame encryptor.

    Encryptor::protocol_version

    fn Encryptor::protocol_version(self : Encryptor) -> UInt16

    Return the protocol version selected by the active outbound ratchet.

    Encryptor::set_key_ratchet

    fn Encryptor::set_key_ratchet(self : Encryptor, key_ratchet : KeyRatchet) -> Unit raise DaveError

    Install a copy of key_ratchet for outbound media.

    Encryptor::set_passthrough_mode

    fn Encryptor::set_passthrough_mode(self : Encryptor, enabled~ : Bool) -> Unit raise DaveError

    Enable or disable unencrypted passthrough.

    KeyRatchet

    pub struct KeyRatchet {
    // private fields
    }

    A key ratchet derived from an established MLS session.

    Installing a ratchet into an encryptor or decryptor copies its native key state. The KeyRatchet therefore remains independently owned and may be collected after installation.

    MediaType

    pub(all) enum MediaType {
    Audio
    Video
    } derive(Eq,
    Debug
    )

    The kind of media carried by a DAVE frame.

    MlsFailure

    pub(all) struct MlsFailure {
    source : String
    reason : String
    } derive(Eq,
    Debug
    )

    Details captured from libdave's MLS failure callback.

    RosterChange

    pub(all) enum RosterChange {
    Upsert(user_id~ : UInt64, signature_key~ : Bytes)
    Remove(user_id~ : UInt64)
    } derive(Eq,
    Debug
    )

    A change to the P-256 signature-public-key roster returned by an applied MLS commit or Welcome.

    Session

    pub struct Session {
    // private fields
    }

    A stateful MLS session.

    The native handle is owned by this value and released by a finalizer. A copied Session aliases the same mutable native session, so calls on the same session must be serialized by the application.

    Session::current_group_pairwise_fingerprint_blocking

    fn Session::current_group_pairwise_fingerprint_blocking(self : Session, user_id~ : UInt64) -> Bytes raise DaveError

    Compute a pairwise fingerprint for the currently established MLS group.

    This proves only that the two signing keys in the current group produce the same comparison value. With the official v1.2.0 prebuilt C ABI, signing identities differ between concurrent Session values and rotate on every reinitialize, so this is not persistent identity verification or DAVE identity continuity. Discard the result when the session is reinitialized or replaced. The DAVE fingerprint-format version is fixed to zero by this API, as required by the canonical protocol.

    The operation is deliberately synchronous and may block for an expensive scrypt computation. Do not call it on a latency-sensitive cooperative event loop.

    Session::key_package

    fn Session::key_package(self : Session) -> Bytes raise DaveError

    Create a fresh, single-use marshalled MLS key package.

    Session::key_ratchet

    fn Session::key_ratchet(self : Session, user_id~ : UInt64) -> KeyRatchet? raise DaveError

    Derive a media key ratchet for user_id from the established MLS epoch.

    Session::last_epoch_authenticator

    fn Session::last_epoch_authenticator(self : Session) -> Bytes? raise DaveError

    Return the authenticator of the established MLS epoch, if one exists.

    Session::new

    fn Session::new(protocol_version~ : UInt16, group_id~ : UInt64, self_user_id~ : UInt64) -> Session raise DaveError

    Create and initialize an MLS session.

    The official libdave v1.2.0 prebuilt C ABI has persistent keys disabled, so each Session receives a new transient signing identity. Persisted native authentication-session IDs are intentionally not exposed by this v0.1 API.

    Session::process_commit

    fn Session::process_commit(self : Session, commit : Bytes) -> CommitResult raise DaveError

    Process an incoming MLS commit without collapsing its protocol outcome into an exception. An empty serialized commit is rejected before FFI.

    Session::process_proposals

    fn Session::process_proposals(self : Session, proposals : Bytes, recognized_user_ids~ : ArrayView[UInt64]) -> Bytes raise DaveError

    Process a complete libdave proposals payload.

    The nonempty payload includes its leading revoke/append byte. On success libdave returns one serialized commit followed by an optional serialized Welcome. Processing requires pending or established MLS group state, normally created by set_external_sender after initialization. A successful call always returns a nonempty outbound commit/Welcome payload. recognized_user_ids is the caller's explicit identity-recognition policy; the wrapper adds this session's local user ID when it is absent.

    Session::process_welcome

    fn Session::process_welcome(self : Session, welcome : Bytes, recognized_user_ids~ : ArrayView[UInt64]) -> WelcomeResult raise DaveError

    Process an incoming MLS Welcome without collapsing protocol rejection into an exception. An applied result carries a roster delta: an Upsert has a signature key and a Remove represents an empty native signature. recognized_user_ids must contain the IDs that the caller recognizes for this group; the wrapper adds this session's local user ID when it is absent. An empty serialized Welcome is rejected before FFI.

    Session::protocol_version

    fn Session::protocol_version(self : Session) -> UInt16

    Return the session's current protocol version.

    Session::reinitialize

    fn Session::reinitialize(self : Session, protocol_version~ : UInt16, group_id~ : UInt64, self_user_id~ : UInt64) -> Unit raise DaveError

    Reset and initialize this session for a new group context.

    This invokes libdave's session initialization again on the same native handle. libdave retains a configured external sender and uses it to create the new pending group state. The official v1.2.0 prebuilt C ABI also generates a new transient signing identity on every reinitialization, so verification state from the previous group must not be carried forward.

    Session::reset

    fn Session::reset(self : Session) -> Unit raise DaveError

    Clear this session's MLS group state and group context.

    libdave retains a configured external sender, so a later reinitialize can create pending group state without setting the sender again.

    Session::set_external_sender

    fn Session::set_external_sender(self : Session, external_sender : Bytes) -> Unit raise DaveError

    Configure the serialized MLS external sender package.

    The sender may only be changed while there is no pending or established MLS group state. Call reset first when replacing a sender, then reinitialize to create the new pending group state.

    Session::set_protocol_version

    fn Session::set_protocol_version(self : Session, protocol_version~ : UInt16) -> Unit raise DaveError

    Set the protocol version recorded by this session. Zero selects the unencrypted protocol state; higher values must not exceed the loaded library's supported maximum.

    WelcomeResult

    pub(all) enum WelcomeResult {
    Applied(changes~ : Array[RosterChange])
    Failed(failure~ : MlsFailure)
    } derive(Eq,
    Debug
    )

    The protocol outcome of processing an MLS Welcome.

    available

    fn available() -> Bool

    Whether the bundled native libdave runtime is available to this process.

    max_supported_protocol_version

    fn max_supported_protocol_version() -> UInt16

    Return the maximum DAVE protocol version supported by the loaded libdave.

    This returns zero when the native library is unavailable. It does not identify the libdave ABI or release version.

    pairwise_verification_code

    fn pairwise_verification_code(fingerprint : Bytes) -> String raise DaveError

    Convert a pairwise fingerprint into DAVE's standard 45-digit verification code.

    The input must contain at least 45 bytes. Additional fingerprint bytes are intentionally ignored by the standard display-code algorithm. This function only formats bytes; it does not establish their session provenance or make a current-group fingerprint persistent across a session replacement or reinitialization.

    unavailable_reason

    fn unavailable_reason() -> String?

    Explain why native libdave support is unavailable, if it is unavailable.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io