Skip to content

C and zig bindings#205

Open
dannym-arx wants to merge 22 commits intomasterfrom
c-bindings
Open

C and zig bindings#205
dannym-arx wants to merge 22 commits intomasterfrom
c-bindings

Conversation

@dannym-arx
Copy link
Contributor

@dannym-arx dannym-arx commented Feb 28, 2026

image

Someone at the marmot house casually whispered something about C and Zig bindings into the void... soo... naturally I built it because why not

then hzrd said why not do JS too, so I added those... so obviously I threw those in too

and technically the C bindings now make it super easy to do bindings in other languages too... like go and php...

because marmots are legally and morally obligated to inject MLS into absolutely everything, or something, I don't know

⚠️ Security-sensitive changes

This PR adds first-class FFI bindings for C, Zig, and JavaScript (Bun/Deno) and the CI packaging pipeline to build and publish those bindings, enabling MDK consumers in non‑Rust ecosystems. It exposes a C ABI for core MDK functionality, provides high‑level JS and Zig wrappers that mirror the Rust API, and wires CI jobs to build native artifacts and publish per-language packages.

What changed:

  • Added a new crate crates/mdk-cbindings implementing a complete C FFI (opaque MdkHandle, MdkError, 37+ extern "C" functions for lifecycle, groups, messages, welcomes, key packages, media, memory helpers, and cbindgen header generation).
  • Added crates/mdk-js with Bun and Deno FFI backends, a high‑level Mdk JS wrapper (static constructors, instance methods for groups/messages/welcomes/key packages, media free functions), symbols table, and runtime-specific entrypoints.
  • Added crates/mdk-zig with a Zig wrapper around the C API (RAII Mdk type, owned CString/CBytes types, error union, media helpers) plus Zig build manifests and tests.
  • Introduced .github/workflows/package-mdk-bindings.yml to build native libraries across OS matrices and publish language-specific packages to remote repos (CBINDINGS_REPO, ZIG_REPO, JS_REPO).
  • Updated Cargo workspace to exclude mdk-zig and mdk-js and added .gitignore entries for generated headers and Zig build artifacts.
  • Enabled serde deserialization for MdkConfig in crates/mdk-core to allow JSON config from FFI boundaries.

Security impact:

  • C FFI functions are wrapped with panic safety (catch_unwind), null-pointer guards, and thread-local last-error messages to mitigate UB and cross-thread leakage.
  • New memory-management functions (mdk_string_free, mdk_bytes_free) centralize safe deallocation for FFI-returned data.
  • Media APIs expose encryption/decryption and key-derivation surfaces (prepare_group_image, decrypt_group_image, derive_upload_keypair), requiring careful handling of keys, nonces, and secret zeroization (zeroize applied to transient secrets).
  • MdkHandle is mutex-wrapped to protect concurrent access to the core MDK instance across FFI calls.

Protocol changes:

  • No MLS protocol algorithm or core behavior changes were introduced; changes are language bindings and config deserialization only.

API surface:

  • New public C ABI: mdk_new / mdk_new_with_key / mdk_new_unencrypted, mdk_free, mdk_last_error_message, md*_... functions for groups/messages/welcomes/key packages/media, plus memory free helpers and the MdkError enum.
  • New JS public API: exported setBackend, MdkError, ErrorCode, Mdk class with create/createWithKey/createUnencrypted and full instance methods, plus free functions prepareGroupImage, decryptGroupImage, deriveUploadKeypair.
  • New Zig public API: Mdk type with constructors, owned CString/CBytes types, Error union and mapping, methods mirroring the JS/Rust surface, and free media functions.
  • MdkConfig is now JSON-deserializable, enabling configuration via FFI-passed JSON strings.

Testing:

  • Unit tests added across bindings: C crate tests for error mapping, memory frees, panic/unwind handling, and FFI round-trips; Zig tests for CString/CBytes ownership and error mapping; JS helpers prepared for FFI integration tests.

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds comprehensive multi-language FFI bindings (C, JS for Bun/Deno, Zig) via a new mdk-cbindings crate, language-specific wrappers, build tooling (cbindgen, Zig), documentation, workspace adjustments, .gitignore updates, and a GitHub Actions workflow to build and publish native artifacts and language packages to remote repos.

Changes

Cohort / File(s) Summary
Workflows & repo config
.github/workflows/package-mdk-bindings.yml, Cargo.toml, .gitignore
New packaging/publishing GH Actions workflow for native + C/Zig/JS bindings; added CBINDINGS_REPO/ZIG_REPO/JS_REPO envs; workspace excludes for zig/js; gitignore entries for generated headers and zig cache/target.
C bindings crate & build
crates/mdk-cbindings/Cargo.toml, crates/mdk-cbindings/build.rs, crates/mdk-cbindings/cbindgen.toml
New crate manifest, cbindgen configuration, and build script to generate include/mdk.h during build.
C FFI core surface
crates/mdk-cbindings/src/lib.rs, crates/mdk-cbindings/src/types.rs, crates/mdk-cbindings/src/error.rs, crates/mdk-cbindings/src/free.rs
Opaque MdkHandle, lifecycle constructors (new/with_key/unencrypted), mdk_free, thread-local last-error API, error enum, and memory free helpers for strings/bytes.
C FFI domain APIs
crates/mdk-cbindings/src/groups.rs, .../messages.rs, .../welcomes.rs, .../key_packages.rs, .../media.rs
Extensive extern "C" APIs for groups, messages (pagination/sort), welcomes, key packages, and media (prepare/decrypt/derive); JSON in/out conventions and input validation.
JavaScript bindings & backends
crates/mdk-js/src/mdk.js, .../index.js, .../mod.ts, .../ffi_bun.js, .../ffi_deno.ts, .../symbols.js
Runtime-agnostic symbol table, Bun and Deno FFI backends, high-level Mdk JS API (constructors, groups/messages/welcomes/key packages/media), out-parameter helpers and backend registration.
Zig bindings
crates/mdk-zig/src/mdk.zig, crates/mdk-zig/src/c.zig, crates/mdk-zig/build.zig, crates/mdk-zig/build.zig.zon
Zig wrapper around C bindings with RAII-owned types, error mapping, constructors, full MDK surface, and build/test integration linking Rust artifacts.
Docs & changelog
crates/mdk-cbindings/README.md, crates/mdk-cbindings/CHANGELOG.md, crates/mdk-js/README.md, crates/mdk-zig/README.md
New READMEs and changelog documenting API surface, install/build instructions, and examples.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant JSWrapper as JS Wrapper (mdk.js)
    participant FFIBackend as FFI Backend (BunFfi/DenoFfi)
    participant NativeLib as Native Library (libmdk)
    participant Memory as Memory Manager (C strings/bytes)

    Client->>JSWrapper: Mdk.create(dbPath, ...)
    JSWrapper->>FFIBackend: callWithStringOut(mdk_new, ...)
    FFIBackend->>NativeLib: mdk_new (C ABI)
    NativeLib->>NativeLib: Initialize handle + storage
    NativeLib-->>FFIBackend: error code + out ptr
    FFIBackend->>Memory: readAndFreeString(ptr)
    Memory-->>FFIBackend: parsed handle
    FFIBackend-->>JSWrapper: handle returned
    JSWrapper-->>Client: Mdk object

    Client->>JSWrapper: getGroups()
    JSWrapper->>FFIBackend: callWithJsonOut(mdk_get_groups, handle)
    FFIBackend->>NativeLib: mdk_get_groups
    NativeLib->>NativeLib: Lock + query groups
    NativeLib-->>FFIBackend: error code + json ptr
    FFIBackend->>Memory: readAndFreeString(ptr)
    Memory-->>FFIBackend: parsed JSON
    FFIBackend-->>JSWrapper: groups
    JSWrapper-->>Client: JS objects
Loading
sequenceDiagram
    participant GH as GitHub Actions
    participant Build as build-ffi-native
    participant PublishC as publish-cbindings
    participant PublishZ as publish-zig
    participant PublishJS as publish-js
    participant RepoC as CBINDINGS_REPO
    participant RepoZ as ZIG_REPO
    participant RepoJS as JS_REPO

    GH->>Build: triggered (release)
    Build->>Build: multi-OS native builds
    Build->>Build: prepare artifacts & upload
    Build-->>PublishC: artifacts available
    PublishC->>PublishC: assemble C package
    PublishC->>RepoC: checkout + commit + conditional tag push
    Build-->>PublishZ: artifacts available
    PublishZ->>PublishZ: assemble Zig package
    PublishZ->>RepoZ: checkout + commit + conditional tag push
    Build-->>PublishJS: artifacts available
    PublishJS->>PublishJS: assemble JS package
    PublishJS->>RepoJS: checkout + commit + conditional tag push
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

security

Suggested reviewers

  • erskingardner
  • jgmontoya
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
No Sensitive Identifier Leakage ✅ Passed The pull request does not leak sensitive identifiers in tracing, formatting, or panic calls. Sensitive data like mls_group_id and encryption keys are excluded from logs and user-visible error messages.
Title check ✅ Passed The title 'C and zig bindings' accurately summarizes the primary change: introducing C and Zig language bindings for MDK, though it omits the JavaScript bindings also added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch c-bindings

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

❌ Coverage: 91.07% → 87.53% (-3.54%)

@github-actions
Copy link

❌ Coverage: 91.07% → 87.53% (-3.54%)

@github-actions
Copy link

❌ Coverage: 91.07% → 87.53% (-3.54%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/package-mdk-bindings.yml (1)

611-611: ⚠️ Potential issue | 🟡 Minor

Inconsistent actions/checkout version — standardize with other steps.

Line 611 uses actions/checkout@v6, but all other 16 checkout steps in this workflow use @v4. While v6 does exist, this inconsistency should be resolved for maintainability.

🔧 Proposed fix
     - name: Checkout Kotlin package repo
-      uses: actions/checkout@v6
+      uses: actions/checkout@v4
       with:
         repository: ${{ env.KOTLIN_PACKAGE_REPO }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/package-mdk-bindings.yml at line 611, The workflow
contains an inconsistent checkout action — the step currently using
actions/checkout@v6 should be changed to actions/checkout@v4 to match the other
checkout steps; locate the step that references actions/checkout@v6 and update
the version tag to `@v4` so all checkout uses are standardized (ensure no other
steps reference `@v6`).
🧹 Nitpick comments (8)
crates/mdk-js/README.md (1)

15-21: Add language specifier to fenced code block.

Per coding guidelines, fenced code blocks should have a language specified. Use text or plaintext for directory listings.

📝 Suggested fix
-```
+```text
 src/          # JS source
 native/       # prebuilt libmdk per platform
   linux-x86_64/libmdk.so
   macos-aarch64/libmdk.dylib
   windows-x86_64/mdk.dll
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @crates/mdk-js/README.md around lines 15 - 21, Update the fenced code block
in the README.md directory listing to include a language specifier (use "text"
or "plaintext") so the block starts with text instead of ; locate the
block shown under the directory listing (the triple-backtick fenced block
containing "src/ # JS source" and the native/ lines) and replace the opening
fence accordingly to comply with the coding guideline.


</details>

</blockquote></details>
<details>
<summary>crates/mdk-zig/README.md (1)</summary><blockquote>

`66-82`: **Consider documenting single-threaded usage requirement.**

Based on learnings from the mdk-uniffi crate, MDK instances should be used in a single-threaded context only. This limitation should be mentioned in the documentation to prevent misuse in concurrent Zig applications.



<details>
<summary>📝 Suggested addition after line 82</summary>

```diff
 // Always call deinit when done
 defer m.deinit();
 ```
+
+> ⚠️ **Thread Safety**: The `Mdk` instance is not thread-safe and should only be used from a single thread.
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/README.md` around lines 66 - 82, Add a short note in the
README near the Mdk usage examples that the Mdk instance is not thread-safe and
must be used from a single thread; update the section containing the example
calls (Mdk.init, Mdk.initWithKey, Mdk.initUnencrypted) and the reminder to call
m.deinit() to include a warning like "The Mdk instance is not thread-safe and
should only be used from a single thread" so consumers know to avoid sharing Mdk
across threads.
```

</details>

</blockquote></details>
<details>
<summary>crates/mdk-js/src/ffi_bun.js (1)</summary><blockquote>

`119-129`: **Potential precision loss when converting pointer to `Number`.**

`Number(buf[0])` converts a `BigUint64` to a JavaScript `Number`. On 64-bit systems with addresses above `Number.MAX_SAFE_INTEGER` (2^53), this could lose precision and produce incorrect pointer values. While most user-space addresses are below this threshold, it's a latent risk.



Consider keeping the value as `BigInt` if Bun's FFI layer supports it, or document the limitation.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/ffi_bun.js` around lines 119 - 129, The read() function in
allocOutPtr (using BigUint64Array buf) converts buf[0] to a JavaScript Number
which can lose precision for 64-bit pointers; change read() to return the raw
BigInt (e.g., return buf[0]) instead of Number(buf[0]) and update any callers of
allocOutPtr().read() to accept a BigInt pointer (or, if the surrounding Bun FFI
cannot handle BigInt, add a clear comment documenting the precision limitation
and assert/guard for addresses <= Number.MAX_SAFE_INTEGER). Reference:
allocOutPtr, buf, read, BigUint64Array, ptr.
```

</details>

</blockquote></details>
<details>
<summary>crates/mdk-cbindings/src/media.rs (1)</summary><blockquote>

`79-90`: **Consider simplifying `.as_ref().clone()` to `.to_vec()` or similar.**

Line 80 uses `prepared.encrypted_data.as_ref().clone()`, which is an unusual pattern. If `encrypted_data` is already a `Vec<u8>` or implements `Clone`, a simpler pattern would be more idiomatic.



However, if `encrypted_data` is a wrapper type where `as_ref()` returns `&Vec<u8>`, this pattern is correct.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-cbindings/src/media.rs` around lines 79 - 90, The field assignment
for PreparedImageJson.encrypted_data uses the odd pattern
prepared.encrypted_data.as_ref().clone(); replace it with a clearer conversion
depending on the type: if prepared.encrypted_data implements AsRef<[u8]> use
prepared.encrypted_data.as_ref().to_vec() or prepared.encrypted_data.to_vec(),
otherwise use prepared.encrypted_data.clone(); update the assignment to use one
of these idiomatic forms (refer to PreparedImageJson and the
prepared.encrypted_data expression) so the conversion is explicit and readable.
```

</details>

</blockquote></details>
<details>
<summary>crates/mdk-cbindings/src/lib.rs (1)</summary><blockquote>

`11-14`: **Reconsider `#![deny(unsafe_code)]` immediately followed by `#![allow(unsafe_code)]`.**

The current pattern effectively disables the lint crate-wide. Per coding guidelines, crates should use `#![forbid(unsafe_code)]` or `#![deny(unsafe_code)]`. For FFI crates where unsafe is necessary, the idiomatic approach is:

1. Keep `#![deny(unsafe_code)]` at crate root
2. Use `#[allow(unsafe_code)]` on individual `extern "C"` functions that require it

This ensures any accidental unsafe code in non-FFI helpers is still flagged.


<details>
<summary>♻️ Suggested approach</summary>

```diff
-#![deny(unsafe_code)]
-// We allow `unsafe` only in explicitly marked blocks — the deny is overridden
-// per-function with `#[allow(unsafe_code)]` where FFI requires it.
-#![allow(unsafe_code)]
+#![deny(unsafe_code)]
 #![warn(missing_docs)]
```

Then on each FFI function:
```rust
#[allow(unsafe_code)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mdk_new(...) -> MdkError {
```
</details>

Based on learnings: "All crates use `#![forbid(unsafe_code)]` or `#![deny(unsafe_code)]` - flag any `unsafe` blocks."

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-cbindings/src/lib.rs` around lines 11 - 14, Remove the crate-level
override that disables the deny (delete `#![allow(unsafe_code)]`) so
`#![deny(unsafe_code)]` is effective, and instead add `#[allow(unsafe_code)]`
attributes directly on each FFI function (the `pub unsafe extern "C"
fn`/`#[no_mangle]` functions such as mdk_new and other extern "C" entry points)
that legitimately require unsafe; this keeps the crate-wide lint while scoping
allowed unsafe to only the specific FFI functions.
```

</details>

</blockquote></details>
<details>
<summary>crates/mdk-js/src/ffi_deno.ts (2)</summary><blockquote>

`27-38`: **TYPE_MAP is missing entries for some NativeType values.**

The `NativeType` union includes `isize`, `f32`, `f64`, `pointer`, and `buffer`, but `TYPE_MAP` only maps a subset. If `symbols.js` uses any unmapped types, `toDenoSymbols` will produce `undefined` entries, causing runtime failures.

Compare with `ffi_bun.js` which includes `i32` but also lacks some types. Ensure all types used in `symbols.js` are covered.

<details>
<summary>♻️ Proposed fix to add missing type mappings</summary>

```diff
 const TYPE_MAP: Record<string, NativeType> = {
   ptr: "pointer",
   cstr: "buffer",
   u8: "u8",
   u16: "u16",
   u32: "u32",
   u64: "u64",
   usize: "usize",
+  isize: "isize",
   bool: "bool",
   i32: "i32",
+  f32: "f32",
+  f64: "f64",
   void: "void",
 };
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/ffi_deno.ts` around lines 27 - 38, TYPE_MAP is incomplete
and can return undefined for NativeType entries used by toDenoSymbols; update
TYPE_MAP to include mappings for all NativeType variants (isize, f32, f64,
pointer, buffer and any other types present in the NativeType union and
symbols.js) so toDenoSymbols always produces valid Deno FFI types. Locate the
TYPE_MAP constant and add the missing keys with their corresponding Deno-native
values (e.g., map pointer/isize/buffer/f32/f64 appropriately), then run tests or
inspect toDenoSymbols consumers to confirm no undefined entries remain.
```

</details>

---

`66-71`: **Platform detection hardcodes CPU architecture.**

Darwin is assumed to be `aarch64` and Linux/Windows are assumed to be `x86_64`. This will fail on Intel Macs or ARM Linux/Windows systems.

<details>
<summary>♻️ Proposed fix to detect architecture dynamically</summary>

```diff
-  const platform =
-    Deno.build.os === "darwin"
-      ? "macos-aarch64"
-      : Deno.build.os === "windows"
-        ? "windows-x86_64"
-        : "linux-x86_64";
+  const arch = Deno.build.arch === "aarch64" ? "aarch64" : "x86_64";
+  const osName =
+    Deno.build.os === "darwin"
+      ? "macos"
+      : Deno.build.os === "windows"
+        ? "windows"
+        : "linux";
+  const platform = `${osName}-${arch}`;
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/ffi_deno.ts` around lines 66 - 71, The platform detection
currently hardcodes CPU architecture in the `platform` ternary (using
`Deno.build.os` only); update it to use `Deno.build.arch` as well and construct
the platform string dynamically (e.g., combine `Deno.build.os` and a mapped
`Deno.build.arch` value) so macOS can resolve to `macos-x86_64` or
`macos-aarch64` and Linux/Windows get the correct architecture too; modify the
`platform` expression to consult `Deno.build.arch` (or map its values like
"x86_64" → "x86_64", "aarch64" → "aarch64") and then produce the final strings
used elsewhere in this file (`platform`).
```

</details>

</blockquote></details>
<details>
<summary>crates/mdk-js/src/mdk.js (1)</summary><blockquote>

`297-313`: **Consider caching the ffi() result to avoid repeated calls.**

These methods call `ffi()` twice each. While the overhead is minimal (it's just checking `_ffi`), caching the result would be more consistent with other methods.

<details>
<summary>♻️ Optional: Cache ffi() result</summary>

```diff
   /** Merge pending commit. `@param` {string} mlsGroupId */
   mergePendingCommit(mlsGroupId) {
-    const cGid = ffi().toCString(mlsGroupId);
-    check(ffi().sym.mdk_merge_pending_commit(this.#handle, cGid.ptr));
+    const f = ffi();
+    const cGid = f.toCString(mlsGroupId);
+    check(f.sym.mdk_merge_pending_commit(this.#handle, cGid.ptr));
   }

   /** Clear pending commit. `@param` {string} mlsGroupId */
   clearPendingCommit(mlsGroupId) {
-    const cGid = ffi().toCString(mlsGroupId);
-    check(ffi().sym.mdk_clear_pending_commit(this.#handle, cGid.ptr));
+    const f = ffi();
+    const cGid = f.toCString(mlsGroupId);
+    check(f.sym.mdk_clear_pending_commit(this.#handle, cGid.ptr));
   }

   /** Sync group metadata from MLS. `@param` {string} mlsGroupId */
   syncGroupMetadata(mlsGroupId) {
-    const cGid = ffi().toCString(mlsGroupId);
-    check(ffi().sym.mdk_sync_group_metadata(this.#handle, cGid.ptr));
+    const f = ffi();
+    const cGid = f.toCString(mlsGroupId);
+    check(f.sym.mdk_sync_group_metadata(this.#handle, cGid.ptr));
   }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/mdk.js` around lines 297 - 313, The three methods
mergePendingCommit, clearPendingCommit, and syncGroupMetadata call ffi() twice
each; cache the ffi() result at the start of each method (e.g., const f = ffi())
and use f.sym.* for the check calls so you don't call ffi() again, keeping the
existing cGid handling and check(…) usage unchanged.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @crates/mdk-cbindings/CHANGELOG.md:

  • Around line 7-19: The Unreleased changelog only contains an "Added" list
    without the required Keep a Changelog sections and missing PR link suffixes;
    update crates/mdk-cbindings/CHANGELOG.md by adding the standard Keep a Changelog
    sections (e.g., Added, Changed, Fixed, Removed as applicable) under "Unreleased"
    and append PR reference links to each bullet in the format
    (#123); ensure each bullet from the
    current Added list (e.g., "Initial C ABI bindings for mdk-core with SQLite
    storage", "33-function API surface mirroring mdk-uniffi exactly", "Opaque
    MdkHandle...", "JSON serialization...", "Thread-local error message
    storage...", "Panic safety via catch_unwind...", "Null pointer guards...",
    "mdk_string_free...", "cbindgen-based header generation (include/mdk.h)")
    has a corresponding PR number/link and move any non-Added items into their
    appropriate sections per the project changelog guidelines.

In @crates/mdk-cbindings/README.md:

  • Around line 17-22: Update the fenced code block in the README.md that lists
    the generated artifacts to include a language specifier (e.g., change the
    opening "" to "text"); locate the block containing the lines
    "include/mdk.h", "lib/linux-x86_64/libmdk.so", "lib/macos-aarch64/libmdk.dylib",
    and "lib/windows-x86_64/mdk.dll" and add the specifier so the block starts with
    "```text" to satisfy markdown linting rules.

In @crates/mdk-cbindings/src/types.rs:

  • Around line 67-84: The ffi_catch wrapper currently extracts and forwards panic
    payload content to set_last_error, risking sensitive data leakage; change
    ffi_catch (the function using std::panic::catch_unwind) so that on Err(payload)
    it does NOT include the payload text in set_last_error but records a generic
    message (e.g., "Internal panic occurred") and optionally logs the payload
    locally (not via set_last_error) for debugging; ensure the function still
    returns MdkError::Mdk and keep the generic message in mdk_last_error_message()
    instead of any panic payload.

In @crates/mdk-cbindings/src/welcomes.rs:

  • Around line 60-66: The Welcome objects returned by to_json(&welcomes) expose
    sensitive identifiers; update the Welcome struct (in
    crates/mdk-storage-traits/src/welcomes/types.rs) to prevent serialization of the
    mls_group_id and nostr_group_id fields by annotating both fields with
    #[serde(skip_serializing)], then rebuild and ensure callers (e.g., the
    get_pending_welcomes path used by lock_handle(...).get_pending_welcomes and the
    to_json call) still compile and return JSON without those fields.

In @crates/mdk-js/src/ffi_bun.js:

  • Around line 41-46: The platform mapping in the const platform block
    incorrectly assumes darwin is always macos-aarch64; update the detection to also
    check process.arch (e.g., process.arch === "arm64" vs "x64") and map darwin to
    "macos-aarch64" for arm64 and "macos-x86_64" (or "macos-x86-64" matching your
    build names) for x86_64 so the platform constant correctly reflects Intel vs
    Apple Silicon Macs; adjust the ternary/branch around process.platform ===
    "darwin" in the platform variable to use process.arch for the darwin case.

Outside diff comments:
In @.github/workflows/package-mdk-bindings.yml:

  • Line 611: The workflow contains an inconsistent checkout action — the step
    currently using actions/checkout@v6 should be changed to actions/checkout@v4 to
    match the other checkout steps; locate the step that references
    actions/checkout@v6 and update the version tag to @v4 so all checkout uses are
    standardized (ensure no other steps reference @v6).

Nitpick comments:
In @crates/mdk-cbindings/src/lib.rs:

  • Around line 11-14: Remove the crate-level override that disables the deny
    (delete #![allow(unsafe_code)]) so #![deny(unsafe_code)] is effective, and
    instead add #[allow(unsafe_code)] attributes directly on each FFI function
    (the pub unsafe extern "C" fn/#[no_mangle] functions such as mdk_new and
    other extern "C" entry points) that legitimately require unsafe; this keeps the
    crate-wide lint while scoping allowed unsafe to only the specific FFI functions.

In @crates/mdk-cbindings/src/media.rs:

  • Around line 79-90: The field assignment for PreparedImageJson.encrypted_data
    uses the odd pattern prepared.encrypted_data.as_ref().clone(); replace it with a
    clearer conversion depending on the type: if prepared.encrypted_data implements
    AsRef<[u8]> use prepared.encrypted_data.as_ref().to_vec() or
    prepared.encrypted_data.to_vec(), otherwise use prepared.encrypted_data.clone();
    update the assignment to use one of these idiomatic forms (refer to
    PreparedImageJson and the prepared.encrypted_data expression) so the conversion
    is explicit and readable.

In @crates/mdk-js/README.md:

  • Around line 15-21: Update the fenced code block in the README.md directory
    listing to include a language specifier (use "text" or "plaintext") so the block
    starts with text instead of ; locate the block shown under the directory
    listing (the triple-backtick fenced block containing "src/ # JS source" and the
    native/ lines) and replace the opening fence accordingly to comply with the
    coding guideline.

In @crates/mdk-js/src/ffi_bun.js:

  • Around line 119-129: The read() function in allocOutPtr (using BigUint64Array
    buf) converts buf[0] to a JavaScript Number which can lose precision for 64-bit
    pointers; change read() to return the raw BigInt (e.g., return buf[0]) instead
    of Number(buf[0]) and update any callers of allocOutPtr().read() to accept a
    BigInt pointer (or, if the surrounding Bun FFI cannot handle BigInt, add a clear
    comment documenting the precision limitation and assert/guard for addresses <=
    Number.MAX_SAFE_INTEGER). Reference: allocOutPtr, buf, read, BigUint64Array,
    ptr.

In @crates/mdk-js/src/ffi_deno.ts:

  • Around line 27-38: TYPE_MAP is incomplete and can return undefined for
    NativeType entries used by toDenoSymbols; update TYPE_MAP to include mappings
    for all NativeType variants (isize, f32, f64, pointer, buffer and any other
    types present in the NativeType union and symbols.js) so toDenoSymbols always
    produces valid Deno FFI types. Locate the TYPE_MAP constant and add the missing
    keys with their corresponding Deno-native values (e.g., map
    pointer/isize/buffer/f32/f64 appropriately), then run tests or inspect
    toDenoSymbols consumers to confirm no undefined entries remain.
  • Around line 66-71: The platform detection currently hardcodes CPU architecture
    in the platform ternary (using Deno.build.os only); update it to use
    Deno.build.arch as well and construct the platform string dynamically (e.g.,
    combine Deno.build.os and a mapped Deno.build.arch value) so macOS can
    resolve to macos-x86_64 or macos-aarch64 and Linux/Windows get the correct
    architecture too; modify the platform expression to consult Deno.build.arch
    (or map its values like "x86_64" → "x86_64", "aarch64" → "aarch64") and then
    produce the final strings used elsewhere in this file (platform).

In @crates/mdk-js/src/mdk.js:

  • Around line 297-313: The three methods mergePendingCommit, clearPendingCommit,
    and syncGroupMetadata call ffi() twice each; cache the ffi() result at the start
    of each method (e.g., const f = ffi()) and use f.sym.* for the check calls so
    you don't call ffi() again, keeping the existing cGid handling and check(…)
    usage unchanged.

In @crates/mdk-zig/README.md:

  • Around line 66-82: Add a short note in the README near the Mdk usage examples
    that the Mdk instance is not thread-safe and must be used from a single thread;
    update the section containing the example calls (Mdk.init, Mdk.initWithKey,
    Mdk.initUnencrypted) and the reminder to call m.deinit() to include a warning
    like "The Mdk instance is not thread-safe and should only be used from a single
    thread" so consumers know to avoid sharing Mdk across threads.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

**Configuration used**: Path: .coderabbit.yaml

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0fbb6b4d6364becc7b8a8bf230c0cf88b2ebe7c0 and 57a1c9cc0942b9ee9afa6821120a3dacf48219c4.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (30)</summary>

* `.github/workflows/package-mdk-bindings.yml`
* `.gitignore`
* `Cargo.toml`
* `crates/mdk-cbindings/CHANGELOG.md`
* `crates/mdk-cbindings/Cargo.toml`
* `crates/mdk-cbindings/README.md`
* `crates/mdk-cbindings/build.rs`
* `crates/mdk-cbindings/cbindgen.toml`
* `crates/mdk-cbindings/include/mdk.h`
* `crates/mdk-cbindings/src/error.rs`
* `crates/mdk-cbindings/src/free.rs`
* `crates/mdk-cbindings/src/groups.rs`
* `crates/mdk-cbindings/src/key_packages.rs`
* `crates/mdk-cbindings/src/lib.rs`
* `crates/mdk-cbindings/src/media.rs`
* `crates/mdk-cbindings/src/messages.rs`
* `crates/mdk-cbindings/src/types.rs`
* `crates/mdk-cbindings/src/welcomes.rs`
* `crates/mdk-js/README.md`
* `crates/mdk-js/src/ffi_bun.js`
* `crates/mdk-js/src/ffi_deno.ts`
* `crates/mdk-js/src/index.js`
* `crates/mdk-js/src/mdk.js`
* `crates/mdk-js/src/mod.ts`
* `crates/mdk-js/src/symbols.js`
* `crates/mdk-zig/README.md`
* `crates/mdk-zig/build.zig`
* `crates/mdk-zig/build.zig.zon`
* `crates/mdk-zig/src/c.zig`
* `crates/mdk-zig/src/mdk.zig`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Rust (mdk-cbindings):
- #![deny(unsafe_code)] now actually denies — per-function #[allow] instead of
  crate-level allow that made the deny theatrical
- Deduplicate UpdateGroupResultJson and serializer into types.rs
- Collapse three-layer ffi_catch/ffi_try/ffi_try_unwind_safe into one function
- Extract null-check boilerplate into require_non_null!/deref_handle! macros
  (60 replacements, ~180 lines of ceremony eliminated)
- Eliminate ConfigOverrides: add #[serde(default)] + Deserialize to MdkConfig
  in mdk-core, deserialize directly (fixes u32→usize truncation too)
- Fix mdk_bytes_free leaking zero-length allocations (remove len>0 guard)
- Feature-gate media.rs behind #[cfg(feature = "mip04")]
- Document thread-local error semantics and secret key lifetime limitations
- Add 36 tests for FFI boundary code (was: zero)

JavaScript (mdk-js):
- Platform detection now checks process.arch/Deno.build.arch (ARM Linux, Intel Mac)
- Guard BigInt→Number precision loss in Bun allocOutPtr with MAX_SAFE_INTEGER check
- Add use-after-close guard (#assertOpen on all 25 methods) + FinalizationRegistry
- Remove JSON double-encoding for key package events (single parse pass)

Zig (mdk-zig):
- Replace hand-rolled JSON serializer with std.json.Stringify.valueAlloc
- Initialize all out pointers to null/0 instead of undefined
- Add 7 new tests (sliceToC, configToJson, optSliceToC round-trips)

CI:
- Add validation to version extraction (fail on empty instead of silent)
- Remove 2>/dev/null || true from artifact copies (surface missing files)
- Add mdk.h to .gitignore (generated by cbindgen, shouldn't be tracked)
- Document auto-populated repo links in binding READMEs
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.07% → 88.54% (-2.53%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/package-mdk-bindings.yml:
- Around line 846-847: Replace all occurrences of the force-push invocation git
push origin "v${VERSION}" --force with a non-destructive push (git push origin
"v${VERSION}") so tags are not rewritten; locate the three places where the
workflow runs git push for the v${VERSION} tag (the lines showing git push
origin "v${VERSION}" --force) and remove the --force flag (or implement an
explicit check that the tag does not already exist before creating/pushing it)
to avoid invalidating existing release tags and provenance.
- Around line 705-714: The matrix currently hardcodes OS-only entries under
strategy.matrix.include (the tuples with os and lib) which causes macOS to be
published as macos-aarch64 regardless of runner architecture; update the matrix
to include an explicit arch dimension (e.g., matrix: os: [ubuntu-latest,
macos-latest, windows-latest]; arch: [x86_64, aarch64]) or expand include to
enumerate both macos x86_64 and macos aarch64 entries, and then reference
matrix.arch when naming artifacts and choosing the library filename; locate the
entries referencing strategy.matrix.include and the artifact naming steps and
replace the hardcoded lib values with expressions that depend on matrix.os and
matrix.arch (or a small mapping) so macOS builds are produced and labeled for
both Intel and Apple Silicon.

In `@crates/mdk-cbindings/src/error.rs`:
- Around line 85-93: The current from_storage_error and from_mdk_error call
set_last_error with the full error formatted ("Storage error: {e}" / "MDK error:
{e}"), which can leak sensitive internals; change these to record a generic or
sanitized message instead (e.g., "Storage error: internal" / "MDK error:
internal" or include only a non-sensitive error code), and ensure StorageError
and CoreMdkError are not directly interpolated into set_last_error; update
from_storage_error and from_mdk_error to call set_last_error with the new
generic text while still returning MdkError::Storage and MdkError::Mdk
respectively.

In `@crates/mdk-cbindings/src/media.rs`:
- Around line 24-31: Remove the sensitive upload_secret_key from the JSON
returned by mdk_prepare_group_image: stop serialising the upload_secret_key
field (and any equivalent field used at lines ~87-98) into the response
struct/string, rely on callers using mdk_derive_upload_keypair for derivation,
and ensure the struct used by the JSON serializer no longer contains or emits
upload_secret_key (update the response type/serde attributes and any
Debug/Display implementations accordingly); also update related tests, docs, and
FFI contract/comments to reflect that the secret key is not returned over JSON.
- Around line 157-170: The temporary plaintext buffers key_arr and nonce_arr
must be explicitly wiped after they are wrapped in mdk_storage_traits::Secret
and after core_decrypt usage; update the code around where key_arr and nonce_arr
are created/copied and passed into Secret::new (and the call to core_decrypt) to
call a zeroization method (e.g., zeroize::Zeroize::zeroize or similar) on those
arrays immediately after constructing the Secret instances and/or after
core_decrypt returns; ensure both the occurrences around the first block (lines
showing key_arr/nonce_arr and core_decrypt) and the second occurrence mentioned
(around 216-220) are updated so no plaintext remains on the stack.

In `@crates/mdk-js/README.md`:
- Around line 15-21: The fenced code block showing the project paths (the block
containing the lines "src/", "native/", "linux-x86_64/libmdk.so",
"macos-aarch64/libmdk.dylib", "windows-x86_64/mdk.dll") is missing a language
specifier; update the opening fence for that block in README.md from ``` to
```text (or another appropriate language) so the markdown linter recognizes it
as a code/text block without altering the block content or directory entries.

In `@crates/mdk-js/src/ffi_deno.ts`:
- Around line 57-64: The Windows library basename is wrong: the code builds base
as `libmdk.dll` but the packaged artifact is `mdk.dll`; update the logic that
computes `ext`/`base` in ffi_deno.ts (the constants ext, base, and related path
construction using pkgRoot) so that on Windows the basename is `mdk.dll` (and on
macOS/Linux keep the existing `libmdk.dylib`/`libmdk.so` names), ensuring the
binary lookup uses the actual packaged name.
- Around line 182-190: The early return when len === 0 can leak the native
allocation because mdk_bytes_free is skipped; update the out-bytes handling in
the block that reads ptrBuf/lenBuf (variables pVal and len) so that when pVal
!== 0n you always call ffi.sym.mdk_bytes_free(p, BigInt(len)) before returning
null for zero length (create the UnsafePointer p only when pVal !== 0n, free it
even if len === 0, and only then return null); reference the existing symbols
pVal, lenBuf, Deno.UnsafePointer.create, Deno.UnsafePointerView, and
ffi.sym.mdk_bytes_free to locate and apply the change.
- Around line 27-30: TYPE_MAP currently maps cstr to "buffer" but code passes
toCString(...).ptr (a Deno.UnsafePointer); change the mapping for "cstr" in
TYPE_MAP from "buffer" to "pointer" and update any other duplicate mappings (the
ones around the 123–125 region) so functions that receive toCString(...).ptr are
declared as "pointer" types; keep the existing logic that keeps the original
Uint8Array alive while passing the UnsafePointer (e.g., callers of toCString) so
the pointer remains valid at call time.

In `@crates/mdk-zig/src/mdk.zig`:
- Around line 161-163: Change the Mdk struct's handle field from a raw pointer
to an optional pointer (e.g. ?*raw.MdkHandle), then make Mdk.deinit check for a
non-null handle before freeing and set the handle to null after free; update any
other deinit-like code paths (the other deinit block around the second
occurrence) to follow the same pattern so calling deinit() multiple times is a
no-op rather than UB, and adjust constructors/initializers to store a non-null
optional when creating the Mdk instance.
- Around line 187-190: The wrapper should validate the out pointer even when
raw.mdk_new returns success: after calling try check(raw.mdk_new(..., &out))
verify out is not null and return an appropriate error (or map to
MDK_ERROR_NULL_OUTPUT) instead of unwrapping out.?; update the block around
variable out and the return {.handle = out.?, .allocator = allocator} to
explicitly handle out == null and propagate a failure, and apply the same
null-check pattern to the similar block at the other occurrence (lines around
the second raw.mdk_new call).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57a1c9c and a0982ef.

📒 Files selected for processing (21)
  • .github/workflows/package-mdk-bindings.yml
  • .gitignore
  • PR-REVIEW.md
  • crates/mdk-cbindings/README.md
  • crates/mdk-cbindings/include/mdk.h
  • crates/mdk-cbindings/src/error.rs
  • crates/mdk-cbindings/src/free.rs
  • crates/mdk-cbindings/src/groups.rs
  • crates/mdk-cbindings/src/key_packages.rs
  • crates/mdk-cbindings/src/lib.rs
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-cbindings/src/messages.rs
  • crates/mdk-cbindings/src/types.rs
  • crates/mdk-cbindings/src/welcomes.rs
  • crates/mdk-core/src/lib.rs
  • crates/mdk-js/README.md
  • crates/mdk-js/src/ffi_bun.js
  • crates/mdk-js/src/ffi_deno.ts
  • crates/mdk-js/src/mdk.js
  • crates/mdk-zig/README.md
  • crates/mdk-zig/src/mdk.zig
✅ Files skipped from review due to trivial changes (1)
  • crates/mdk-zig/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • .gitignore
  • crates/mdk-cbindings/src/types.rs
  • crates/mdk-js/src/ffi_bun.js
  • crates/mdk-cbindings/src/lib.rs

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.07% → 88.54% (-2.53%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
crates/mdk-js/src/ffi_deno.ts (1)

64-64: Windows pathname from URL includes leading slash, but Deno APIs handle this transparently.

new URL(".", import.meta.url).pathname returns /C:/Users/... on Windows per the URL standard. While Deno's file APIs like Deno.dlopen handle this format correctly in modern versions, the path is technically not in standard Windows format.

[optional_refactor]

💡 Optional: Use standard Windows path format for clarity
-  const pkgRoot = new URL(".", import.meta.url).pathname;
+  let pkgRoot = new URL(".", import.meta.url).pathname;
+  // Remove leading slash on Windows paths for standard format
+  if (Deno.build.os === "windows" && pkgRoot.match(/^\/[A-Za-z]:\//)) {
+    pkgRoot = pkgRoot.slice(1);
+  }

This improves path readability without being functionally necessary, as Deno already normalizes these paths internally before system calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/ffi_deno.ts` at line 64, The pkgRoot assignment uses new
URL(".", import.meta.url).pathname which yields a POSIX-style leading-slash
Windows path; change this to produce a platform-native path by using the
std/path helper (fromFileUrl) or otherwise converting the file URL to a native
Windows path when Deno.build.os === "windows"; update the pkgRoot initialization
in ffi_deno.ts (the const pkgRoot binding) to call fromFileUrl(new URL(".",
import.meta.url)) so pkgRoot is in standard Windows format on Windows and
unchanged on POSIX.
crates/mdk-zig/src/mdk.zig (1)

269-273: Missing null-check on output pointers for operations returning CString.

While constructors validate output pointers, operations like createKeyPackage, createKeyPackageWithOptions, and parseKeyPackage directly return CString{ .ptr = out } without checking if out is null after the C call succeeds. This is inconsistent with the constructor pattern and could lead to crashes if the C layer has bugs.

Consider adding validation for consistency:

♻️ Example fix for createKeyPackage
         var out: [*c]u8 = null;
         const h = try self.liveHandle();
         try check(raw.mdk_create_key_package(h, c_pk, c_relays, &out));
-        return CString{ .ptr = out };
+        if (out == null) return Error.NullPointer;
+        return CString{ .ptr = out };

Also applies to: 287-291, 301-305

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 269 - 273, The functions
createKeyPackage, createKeyPackageWithOptions, and parseKeyPackage return
CString{ .ptr = out } without verifying that the C-out pointer is non-null;
replicate the constructor pattern by checking that out != null after the
successful raw call (e.g., after raw.mdk_create_key_package/have corresponding
raw calls) and return an appropriate error (or use try) if out is null before
constructing and returning CString; reference the functions
createKeyPackage/createKeyPackageWithOptions/parseKeyPackage, the local variable
out, and the raw.mdk_* calls (and liveHandle()) when adding the null-checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/mdk-cbindings/src/media.rs`:
- Around line 47-49: Update the doc comment for mdk_prepare_group_image to
reflect current behavior: it encrypts the image and derives the upload keypair
but intentionally omits upload secret key material from the returned JSON;
ensure the description explicitly states that only public key/upload metadata
(not the private/secret key) is included in the returned JSON so callers
understand secrets are not returned.
- Around line 75-78: Replace FFI-visible error messages that interpolate
internal errors (e.g., in the core_prepare error mapping where
error::set_last_error(&format!("Prepare group image failed: {e}")) and the
similar occurrences around the other reported spots) with sanitized static
messages or by converting the error via error::from_mdk_error so no internal
details are exposed; keep detailed error logging internal (e.g., use debug/error
logs that are not set via mdk_last_error) but ensure error::set_last_error
receives only a fixed, non-leaking string (e.g., "Prepare group image failed")
or a sanitized mapping function.

In `@crates/mdk-zig/src/mdk.zig`:
- Around line 133-137: In configToJson replace the invalid call to
std.json.Stringify.valueAlloc with the correct std.json.stringifyAlloc API: call
std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false
}) (or adjust the argument order to match the current Zig stdlib signature) and
update error/return handling to match stringifyAlloc's return type; keep the
early return for null config and return the allocated JSON slice from
configToJson.

---

Nitpick comments:
In `@crates/mdk-js/src/ffi_deno.ts`:
- Line 64: The pkgRoot assignment uses new URL(".", import.meta.url).pathname
which yields a POSIX-style leading-slash Windows path; change this to produce a
platform-native path by using the std/path helper (fromFileUrl) or otherwise
converting the file URL to a native Windows path when Deno.build.os ===
"windows"; update the pkgRoot initialization in ffi_deno.ts (the const pkgRoot
binding) to call fromFileUrl(new URL(".", import.meta.url)) so pkgRoot is in
standard Windows format on Windows and unchanged on POSIX.

In `@crates/mdk-zig/src/mdk.zig`:
- Around line 269-273: The functions createKeyPackage,
createKeyPackageWithOptions, and parseKeyPackage return CString{ .ptr = out }
without verifying that the C-out pointer is non-null; replicate the constructor
pattern by checking that out != null after the successful raw call (e.g., after
raw.mdk_create_key_package/have corresponding raw calls) and return an
appropriate error (or use try) if out is null before constructing and returning
CString; reference the functions
createKeyPackage/createKeyPackageWithOptions/parseKeyPackage, the local variable
out, and the raw.mdk_* calls (and liveHandle()) when adding the null-checks.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0982ef and 099afa9.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • .github/workflows/package-mdk-bindings.yml
  • crates/mdk-cbindings/Cargo.toml
  • crates/mdk-cbindings/src/error.rs
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-js/README.md
  • crates/mdk-js/src/ffi_deno.ts
  • crates/mdk-zig/src/mdk.zig

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.07% → 88.53% (-2.54%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
crates/mdk-zig/src/mdk.zig (2)

140-143: ⚠️ Potential issue | 🔴 Critical

Use std.json.stringifyAlloc; std.json.Stringify.valueAlloc is not a valid Zig stdlib call.

Line 142 is a compile blocker with current Zig stdlib API naming.

🛠️ Suggested fix
-    const result = try std.json.Stringify.valueAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
+    const result = try std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 140 - 143, The call to
std.json.Stringify.valueAlloc in configToJson is invalid with the current Zig
stdlib; replace that call with std.json.stringifyAlloc (using the same allocator
and conf, and preserve the options {.emit_null_optional_fields = false}) so
configToJson compiles; update the invocation inside fn configToJson(allocator:
std.mem.Allocator, cfg: ?Config) to call std.json.stringifyAlloc and return its
result.

712-731: ⚠️ Potential issue | 🟠 Major

Validate out_ptr before constructing CBytes.

After Line 718 succeeds, the wrapper still assumes out_ptr is non-null. A defensive null-check prevents creating an invalid owned buffer if the C contract drifts.

🛡️ Suggested hardening
     try check(raw.mdk_decrypt_group_image(
         data.ptr,
         data.len,
         hash_ptr,
         hash_len,
         key,
         32,
         nonce,
         12,
         &out_ptr,
         &out_len,
     ));
+    if (out_ptr == null) return Error.NullPointer;
     return CBytes{ .ptr = out_ptr, .len = out_len };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 712 - 731, After calling
raw.mdk_decrypt_group_image (in the wrapper that returns CBytes), validate that
out_ptr is not null before constructing and returning CBytes; if out_ptr is null
or out_len is zero treat it as an error (return a Zig error or call the existing
error path) so you don't create an invalid owned buffer—add the nil-check for
out_ptr immediately after the try check and handle it consistently with the
function's error-reporting convention.
crates/mdk-cbindings/src/media.rs (1)

78-82: ⚠️ Potential issue | 🔴 Critical

Remove raw {e} interpolation from warning logs in these FFI crypto paths.

At Line 79, Line 168, and Line 218, tracing::warn!(... {e}) can leak internal details from core errors. Keep the warning text static/sanitized in these paths.

🔐 Patch pattern (apply to all three `map_err` blocks)
-        let prepared = core_prepare(image_bytes, mime_str).map_err(|e| {
-            tracing::warn!("Prepare group image failed: {e}");
+        let prepared = core_prepare(image_bytes, mime_str).map_err(|_| {
+            tracing::warn!("Prepare group image failed");
             error::set_last_error("Prepare group image failed");
             MdkError::Mdk
         })?;

As per coding guidelines: "Never log sensitive identifiers ... in tracing::* macros."

Also applies to: 167-171, 217-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-cbindings/src/media.rs` around lines 78 - 82, Remove the raw {e}
interpolation from the three map_err closures and replace it with a
static/sanitized warning message: locate the map_err around core_prepare (the
closure currently calling tracing::warn!("Prepare group image failed: {e}")) and
the two other similar map_err blocks referenced in the comment, and change the
tracing::warn! call to a fixed message like tracing::warn!("Prepare group image
failed") (or an equivalent static text for the other two contexts); keep
error::set_last_error("Prepare group image failed") and returning MdkError::Mdk
unchanged so only the logged text is sanitized while preserving existing error
flow.
🧹 Nitpick comments (3)
crates/mdk-js/src/ffi_deno.ts (2)

47-51: Consider validating unknown types in symbol conversion.

If defs contains a type not present in TYPE_MAP, the mapping silently produces undefined, which may cause confusing runtime errors from Deno.dlopen. A defensive check could improve debuggability.

💡 Suggested defensive check
   for (const [name, def] of Object.entries(defs)) {
+    const parameters = def.params.map((t) => {
+      const mapped = TYPE_MAP[t];
+      if (!mapped) throw new Error(`Unknown FFI type "${t}" in symbol "${name}"`);
+      return mapped;
+    });
+    const result = TYPE_MAP[def.result];
+    if (!result) throw new Error(`Unknown FFI result type "${def.result}" in symbol "${name}"`);
     out[name] = {
-      parameters: def.params.map((t) => TYPE_MAP[t]),
-      result: TYPE_MAP[def.result],
+      parameters,
+      result,
     };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/ffi_deno.ts` around lines 47 - 51, Loop over defs and
validate that every type referenced in def.params and def.result exists in
TYPE_MAP before assigning to out[name]; if any mapping yields undefined, throw a
clear Error that includes the symbol name (name) and the unknown type(s). Update
the conversion logic around the loop that builds out[name] (the code iterating
"for (const [name, def] of Object.entries(defs))") to collect unknown types from
def.params and def.result and fail-fast with a descriptive message (so the
Deno.dlopen consumer sees the exact missing type).

79-85: Consider adding monorepo sibling paths for dev parity with Bun backend.

The Bun backend includes additional candidate paths for monorepo development (mdk-cbindings/target/...). Adding these would provide consistent developer experience across runtimes.

💡 Suggested additional paths
   const candidates = [
     // Packaged layout
     `${pkgRoot}../native/${platform}/${base}`,
     // Development: cargo build output
     `${pkgRoot}../../../target/release/${base}`,
     `${pkgRoot}../../../target/debug/${base}`,
+    // Monorepo sibling
+    `${pkgRoot}../../mdk-cbindings/target/release/${base}`,
+    `${pkgRoot}../../mdk-cbindings/target/debug/${base}`,
   ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/ffi_deno.ts` around lines 79 - 85, The candidates array for
native lookup (variable candidates using pkgRoot, platform, base) is missing
monorepo sibling paths used by the Bun backend; add sibling dev paths that point
to the mdk-cbindings crate build outputs (e.g. sibling
mdk-cbindings/target/release/${base} and mdk-cbindings/target/debug/${base})
alongside the existing cargo target entries so the lookup in the same candidates
array (where candidates is declared) will find monorepo-built artifacts during
development.
crates/mdk-cbindings/include/mdk.h (1)

126-127: Prefer plain C symbol references in header comments over Rust intra-doc links.

Line 126 and Line 439 include Rust-style links (e.g., crate::...) that are not meaningful to C consumers. Consider emitting plain function names in generated comments for portability/readability.

Also applies to: 439-440

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-cbindings/include/mdk.h` around lines 126 - 127, Replace Rust
intra-doc links in the C header comments with plain C symbol names: change
occurrences like `crate::free::mdk_string_free` to just `mdk_string_free` (and
likewise for any other `crate::...` links found around the other comment blocks
such as the ones at lines 439-440) so that the generated comments reference the
C ABI symbols directly and are readable by C consumers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/mdk-zig/src/mdk.zig`:
- Around line 769-772: The test "check returns error on non-OK" is asserting an
error union incorrectly using std.testing.expectEqual; replace that assertion to
use std.testing.expectError so the call to check(raw.MDK_ERROR_STORAGE) is
asserted to return Error.Storage; locate the test block named "check returns
error on non-OK" and change the expectEqual(Error.Storage, result) usage to
std.testing.expectError(Error.Storage, check(raw.MDK_ERROR_STORAGE)) (or
otherwise call expectError with the expression), keeping references to the check
function, raw.MDK_ERROR_STORAGE, and Error.Storage.

---

Duplicate comments:
In `@crates/mdk-cbindings/src/media.rs`:
- Around line 78-82: Remove the raw {e} interpolation from the three map_err
closures and replace it with a static/sanitized warning message: locate the
map_err around core_prepare (the closure currently calling
tracing::warn!("Prepare group image failed: {e}")) and the two other similar
map_err blocks referenced in the comment, and change the tracing::warn! call to
a fixed message like tracing::warn!("Prepare group image failed") (or an
equivalent static text for the other two contexts); keep
error::set_last_error("Prepare group image failed") and returning MdkError::Mdk
unchanged so only the logged text is sanitized while preserving existing error
flow.

In `@crates/mdk-zig/src/mdk.zig`:
- Around line 140-143: The call to std.json.Stringify.valueAlloc in configToJson
is invalid with the current Zig stdlib; replace that call with
std.json.stringifyAlloc (using the same allocator and conf, and preserve the
options {.emit_null_optional_fields = false}) so configToJson compiles; update
the invocation inside fn configToJson(allocator: std.mem.Allocator, cfg:
?Config) to call std.json.stringifyAlloc and return its result.
- Around line 712-731: After calling raw.mdk_decrypt_group_image (in the wrapper
that returns CBytes), validate that out_ptr is not null before constructing and
returning CBytes; if out_ptr is null or out_len is zero treat it as an error
(return a Zig error or call the existing error path) so you don't create an
invalid owned buffer—add the nil-check for out_ptr immediately after the try
check and handle it consistently with the function's error-reporting convention.

---

Nitpick comments:
In `@crates/mdk-cbindings/include/mdk.h`:
- Around line 126-127: Replace Rust intra-doc links in the C header comments
with plain C symbol names: change occurrences like
`crate::free::mdk_string_free` to just `mdk_string_free` (and likewise for any
other `crate::...` links found around the other comment blocks such as the ones
at lines 439-440) so that the generated comments reference the C ABI symbols
directly and are readable by C consumers.

In `@crates/mdk-js/src/ffi_deno.ts`:
- Around line 47-51: Loop over defs and validate that every type referenced in
def.params and def.result exists in TYPE_MAP before assigning to out[name]; if
any mapping yields undefined, throw a clear Error that includes the symbol name
(name) and the unknown type(s). Update the conversion logic around the loop that
builds out[name] (the code iterating "for (const [name, def] of
Object.entries(defs))") to collect unknown types from def.params and def.result
and fail-fast with a descriptive message (so the Deno.dlopen consumer sees the
exact missing type).
- Around line 79-85: The candidates array for native lookup (variable candidates
using pkgRoot, platform, base) is missing monorepo sibling paths used by the Bun
backend; add sibling dev paths that point to the mdk-cbindings crate build
outputs (e.g. sibling mdk-cbindings/target/release/${base} and
mdk-cbindings/target/debug/${base}) alongside the existing cargo target entries
so the lookup in the same candidates array (where candidates is declared) will
find monorepo-built artifacts during development.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 099afa9 and 86f347c.

📒 Files selected for processing (4)
  • crates/mdk-cbindings/include/mdk.h
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-js/src/ffi_deno.ts
  • crates/mdk-zig/src/mdk.zig

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.07% → 88.52% (-2.55%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
crates/mdk-zig/src/mdk.zig (1)

140-143: ⚠️ Potential issue | 🔴 Critical

Use std.json.stringifyAlloc; current API call is invalid.

std.json.Stringify.valueAlloc is not a valid stdlib JSON allocation API and will fail to compile on current Zig stdlib.

✅ Proposed fix
 fn configToJson(allocator: std.mem.Allocator, cfg: ?Config) std.mem.Allocator.Error!?[]const u8 {
     const conf = cfg orelse return null;
-    const result = try std.json.Stringify.valueAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
+    const result = try std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
     return result;
 }
#!/bin/bash
set -euo pipefail

# 1) Show current callsite(s) in this wrapper.
rg -n 'std\.json\.Stringify\.valueAlloc|std\.json\.stringifyAlloc' crates/mdk-zig/src/mdk.zig

# 2) Verify stdlib API names from Zig's std/json/stringify.zig.
curl -fsSL https://raw.githubusercontent.com/ziglang/zig/main/lib/std/json/stringify.zig \
  | rg -n 'pub fn stringifyAlloc|Stringify\.valueAlloc'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 140 - 143, The call to the
non-existent std.json.Stringify.valueAlloc in function configToJson is invalid;
replace it with the correct API std.json.stringifyAlloc by calling
std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false
}) inside configToJson and return its result (keeping the existing return type
Error!?[]const u8). Update the function body in configToJson to use
std.json.stringifyAlloc and ensure any error propagation (try) and the
null-early-return (cfg orelse return null) remain unchanged.
🧹 Nitpick comments (1)
crates/mdk-cbindings/src/error.rs (1)

157-168: Test name and behavior are mismatched.

from_storage_error_sets_message currently tests invalid_input, not from_storage_error, so it won’t catch regressions in storage-error mapping.

🧪 Suggested cleanup
-    fn from_storage_error_sets_message() {
-        // We can't easily construct a StorageError, but we can test
-        // the simpler helpers
+    fn invalid_input_sets_message() {
+        // Tests invalid_input helper behavior.
         let code = invalid_input("bad input");
         assert_eq!(code, MdkError::InvalidInput);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-cbindings/src/error.rs` around lines 157 - 168, The test named
from_storage_error_sets_message is asserting the behavior of invalid_input
instead of exercising from_storage_error; either rename the test to reflect it
checks invalid_input (e.g., invalid_input_sets_message) or change the test body
to actually call from_storage_error with a constructed StorageError and assert
the returned MdkError and message via from_storage_error and
mdk_last_error_message; update references inside the test
(from_storage_error_sets_message, invalid_input, from_storage_error,
mdk_last_error_message) accordingly so the name and assertions match the
behavior being tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/mdk-cbindings/src/media.rs`:
- Around line 78-83: The tracing::debug call inside the core_prepare error map
closure is interpolating the raw error (`{e}`) which may leak sensitive
identifiers; change the closure used with `core_prepare(image_bytes,
mime_str).map_err(|e| { ... })` to log a generic, non-sensitive message (e.g.,
"prepare_group_image failed") and avoid printing `e` directly—if you need an
identifier for debugging, log a sanitized/structured error code or use a
non-sensitive derived property (e.g., an error enum variant or `.kind()`), then
set the last error and return `MdkError::Mdk`; apply the same change pattern to
the similar tracing usages around the other map_err sites referenced (the blocks
around the groups at the other noted locations).

In `@crates/mdk-zig/src/mdk.zig`:
- Around line 1-5: The crate-level docs currently state that "memory management
is handled automatically via RAII-style owned types" which is misleading for
Zig; update the top comments (and the similar text around lines 41-44) to
explicitly state that callers must call deinit() (typically via defer) on owned
types to free resources rather than relying on automatic scope-based
destruction; reference the term "owned types" in the doc text so readers of
mdk.zig know which types require explicit deinit().

---

Duplicate comments:
In `@crates/mdk-zig/src/mdk.zig`:
- Around line 140-143: The call to the non-existent
std.json.Stringify.valueAlloc in function configToJson is invalid; replace it
with the correct API std.json.stringifyAlloc by calling
std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false
}) inside configToJson and return its result (keeping the existing return type
Error!?[]const u8). Update the function body in configToJson to use
std.json.stringifyAlloc and ensure any error propagation (try) and the
null-early-return (cfg orelse return null) remain unchanged.

---

Nitpick comments:
In `@crates/mdk-cbindings/src/error.rs`:
- Around line 157-168: The test named from_storage_error_sets_message is
asserting the behavior of invalid_input instead of exercising
from_storage_error; either rename the test to reflect it checks invalid_input
(e.g., invalid_input_sets_message) or change the test body to actually call
from_storage_error with a constructed StorageError and assert the returned
MdkError and message via from_storage_error and mdk_last_error_message; update
references inside the test (from_storage_error_sets_message, invalid_input,
from_storage_error, mdk_last_error_message) accordingly so the name and
assertions match the behavior being tested.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86f347c and a66716f.

📒 Files selected for processing (5)
  • crates/mdk-cbindings/include/mdk.h
  • crates/mdk-cbindings/src/error.rs
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-js/src/ffi_deno.ts
  • crates/mdk-zig/src/mdk.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/mdk-js/src/ffi_deno.ts

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.09% → 88.56% (-2.53%)

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.09% → 88.7% (-2.39%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/package-mdk-bindings.yml (1)

611-611: ⚠️ Potential issue | 🔴 Critical

actions/checkout@v6 does not exist.

This checkout step uses actions/checkout@v6, but the latest stable version is v4. All other checkout steps in this workflow correctly use v4. This will cause the workflow to fail.

🐛 Proposed fix
     - name: Checkout Kotlin package repo
-      uses: actions/checkout@v6
+      uses: actions/checkout@v4
       with:
         repository: ${{ env.KOTLIN_PACKAGE_REPO }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/package-mdk-bindings.yml at line 611, The workflow step
uses an invalid action version "actions/checkout@v6"; update the checkout step's
uses value to the correct stable release "actions/checkout@v4" (match the other
checkout steps) so the job will run successfully.
♻️ Duplicate comments (2)
crates/mdk-zig/src/mdk.zig (2)

42-44: ⚠️ Potential issue | 🟡 Minor

Clarify lastErrorMessage cleanup docs to require explicit deinit().

Line 44 still implies scope-based cleanup; this type must be explicitly deinitialized.

📝 Proposed doc fix
 /// Returns `null` if no error has been recorded on this thread (or if the
 /// message has already been consumed).  The returned `CString` must be
-/// allowed to go out of scope (its `deinit` frees the C memory).
+/// explicitly deinitialized (e.g. `defer msg.deinit();`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 42 - 44, The docs for
lastErrorMessage incorrectly imply scope-based cleanup; update the comment for
the function/variable named lastErrorMessage to explicitly state that the
returned CString must be explicitly deinit()'d by the caller (no automatic
scope-based free), e.g., replace wording that says it "must be allowed to go out
of scope" with a sentence that callers are required to call CString.deinit() to
free the C memory after use and that null indicates no error or already-consumed
message.

141-144: ⚠️ Potential issue | 🔴 Critical

Replace invalid JSON API call with std.json.stringifyAlloc.

Line 143 uses a non-existent stdlib API and will fail compilation.

🔧 Proposed fix
 fn configToJson(allocator: std.mem.Allocator, cfg: ?Config) std.mem.Allocator.Error!?[]const u8 {
     const conf = cfg orelse return null;
-    const result = try std.json.Stringify.valueAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
+    const result = try std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
     return result;
 }
#!/bin/bash
set -euo pipefail

# Verify current API usage in Zig files.
rg -n --type zig 'std\.json\.Stringify\.valueAlloc|std\.json\.stringifyAlloc'

# Inspect the exact affected block.
sed -n '141,145p' crates/mdk-zig/src/mdk.zig
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 141 - 144, The call to a
non-existent std.json.Stringify.valueAlloc in function configToJson causes
compilation failure; replace it with the correct Zig stdlib API
std.json.stringifyAlloc and pass the same allocator, value, and options (e.g.,
.{ .emit_null_optional_fields = false }) so the function returns the allocated
JSON slice as before. Update the invocation inside configToJson to use
std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false
}) and keep the existing error handling and return path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/mdk-cbindings/src/groups.rs`:
- Around line 74-77: After validating the out_json pointer with
require_non_null!(out_json, "out_json"), immediately clear the caller-visible
output by writing *out_json = std::ptr::null_mut(); (inside an unsafe block) so
any early return/error path cannot leave a stale pointer; apply this change in
each function that accepts out_json (the ffi_try_unwind_safe closures that call
deref_handle! and then require_non_null!(out_json, "out_json") — update all
occurrences listed in the review so the output is set to null at entry.

In `@crates/mdk-cbindings/src/lib.rs`:
- Around line 84-89: Before doing any constructor work inside the
ffi_try_unwind_safe closure, explicitly set the output pointer to null to avoid
returning stale handles: after the require_non_null!(out, "out"); call in each
constructor (the blocks using types::ffi_try_unwind_safe where you convert
db_path/service_id/db_key_id and parse_config), perform an unsafe write like
*out = std::ptr::null_mut(); so that if later conversion/constructor fails the C
caller gets a null handle; apply the same change to the other two constructor
blocks that follow the same pattern.

In `@crates/mdk-cbindings/src/messages.rs`:
- Around line 66-69: Before performing any fallible operations in the
ffi_try_unwind_safe closure, explicitly initialize *out_json to null to avoid
leaving callers with stale pointers; e.g. inside the closure containing
deref_handle!(h) and before parse_group_id or any ?-returning call, add an
unsafe guard that if !out_json.is_null() then *out_json = std::ptr::null_mut();
apply the same change to every function that accepts out_json (the closures
around the symbols handling mls_group_id and the other locations noted) so
out_json is always null on early-return.

In `@crates/mdk-js/src/mdk.js`:
- Around line 16-22: The setBackend function currently allows replacing the
global _ffi which is unsafe; change setBackend(backend) so that if _ffi !== null
it refuses to replace it (throw a clear Error like "MDK: backend already set" or
return early) instead of logging and assigning; ensure any calls to ffi().sym.*
rely on _ffi being immutable after first initialization by preventing
reassignment in setBackend and leaving _ffi assignment only when it was
previously null.
- Around line 134-142: Ensure callers validate fixed-size crypto buffers before
crossing FFI: in createWithKey (which calls mdk_new_with_key) check that key is
exactly 32 bytes and throw a clear JS error if not; likewise add explicit length
checks for any other functions around lines 532–559 that pass buffers to FFI
(the sites that call f.bufferPtr for nonce and hash) to enforce a 12-byte nonce
and an optional 32-byte hash (error if present but wrong size). Perform these
checks in JS prior to calling f.bufferPtr/f.buffer or mdk_* FFI functions and
return/throw immediately to avoid sending incorrectly sized buffers to native
code. Ensure error messages mention the function name (e.g., createWithKey) and
expected sizes to aid debugging.
- Around line 66-72: callWithStringOut currently allocates native memory via
ffi().allocOutString() but calls check() before freeing, which can leak when
check() throws; wrap the FFI allocation in a try/finally so out.readAndFree()
always runs (use finally or catch+rethrow) after calling fn(..., out.ptr) and
check(); apply the same pattern to any similar helpers that use
allocOutBytes/allocOutString (e.g., decryptGroupImage and other allocation
sites) so mdk_string_free/mdk_bytes_free are always invoked on both success and
error paths.

In `@crates/mdk-zig/src/mdk.zig`:
- Line 79: The documentation for CBytes is incorrect: it references slice() but
the public accessor is bytes(); update the doc comment on CBytes (the line
mentioning `deinit()` and the accessor) to reference `bytes()` instead of
`slice()` so it matches the actual public method name (`CBytes.bytes()`),
leaving the rest of the comment intact.

---

Outside diff comments:
In @.github/workflows/package-mdk-bindings.yml:
- Line 611: The workflow step uses an invalid action version
"actions/checkout@v6"; update the checkout step's uses value to the correct
stable release "actions/checkout@v4" (match the other checkout steps) so the job
will run successfully.

---

Duplicate comments:
In `@crates/mdk-zig/src/mdk.zig`:
- Around line 42-44: The docs for lastErrorMessage incorrectly imply scope-based
cleanup; update the comment for the function/variable named lastErrorMessage to
explicitly state that the returned CString must be explicitly deinit()'d by the
caller (no automatic scope-based free), e.g., replace wording that says it "must
be allowed to go out of scope" with a sentence that callers are required to call
CString.deinit() to free the C memory after use and that null indicates no error
or already-consumed message.
- Around line 141-144: The call to a non-existent std.json.Stringify.valueAlloc
in function configToJson causes compilation failure; replace it with the correct
Zig stdlib API std.json.stringifyAlloc and pass the same allocator, value, and
options (e.g., .{ .emit_null_optional_fields = false }) so the function returns
the allocated JSON slice as before. Update the invocation inside configToJson to
use std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields =
false }) and keep the existing error handling and return path.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a66716f and 48c58d0.

📒 Files selected for processing (12)
  • .github/workflows/package-mdk-bindings.yml
  • crates/mdk-cbindings/Cargo.toml
  • crates/mdk-cbindings/src/error.rs
  • crates/mdk-cbindings/src/groups.rs
  • crates/mdk-cbindings/src/lib.rs
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-cbindings/src/messages.rs
  • crates/mdk-cbindings/src/welcomes.rs
  • crates/mdk-js/src/ffi_bun.js
  • crates/mdk-js/src/ffi_deno.ts
  • crates/mdk-js/src/mdk.js
  • crates/mdk-zig/src/mdk.zig
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/mdk-cbindings/src/welcomes.rs
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-cbindings/Cargo.toml
  • crates/mdk-js/src/ffi_bun.js
  • crates/mdk-js/src/ffi_deno.ts

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.09% → 88.7% (-2.39%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/mdk-zig/src/mdk.zig (1)

141-145: ⚠️ Potential issue | 🔴 Critical

std.json.Stringify.valueAlloc is not a valid Zig standard library API.

The Zig standard library provides std.json.stringifyAlloc for JSON serialization, not std.json.Stringify.valueAlloc. This will cause a compilation error.

🐛 Proposed fix
 fn configToJson(allocator: std.mem.Allocator, cfg: ?Config) std.mem.Allocator.Error!?[]const u8 {
     const conf = cfg orelse return null;
-    const result = try std.json.Stringify.valueAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
+    const result = try std.json.stringifyAlloc(allocator, conf, .{ .emit_null_optional_fields = false });
     return result;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-zig/src/mdk.zig` around lines 141 - 145, The call in function
configToJson uses a non-existent API std.json.Stringify.valueAlloc; replace it
with the correct Zig stdlib function std.json.stringifyAlloc and adapt the call
signature to match stringifyAlloc (pass allocator and the value, and any options
via std.json.StringifyOptions if needed) so configToJson compiles; update any
option names to the types expected by std.json.stringifyAlloc and ensure the
return/errset stays consistent with configToJson's signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/package-mdk-bindings.yml:
- Around line 704-714: The matrix entry uses an unavailable runner value
"macos-13"; update the matrix include entry so that the os value is
"macos-15-intel" (i.e., replace the existing "macos-13" string in the matrix
include block) to target Intel macOS builds; keep the surrounding keys (matrix,
include, os, lib) unchanged and only change that os value.

---

Duplicate comments:
In `@crates/mdk-zig/src/mdk.zig`:
- Around line 141-145: The call in function configToJson uses a non-existent API
std.json.Stringify.valueAlloc; replace it with the correct Zig stdlib function
std.json.stringifyAlloc and adapt the call signature to match stringifyAlloc
(pass allocator and the value, and any options via std.json.StringifyOptions if
needed) so configToJson compiles; update any option names to the types expected
by std.json.stringifyAlloc and ensure the return/errset stays consistent with
configToJson's signature.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48c58d0 and e32287f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • .github/workflows/package-mdk-bindings.yml
  • crates/mdk-cbindings/include/mdk.h
  • crates/mdk-cbindings/src/groups.rs
  • crates/mdk-cbindings/src/lib.rs
  • crates/mdk-cbindings/src/messages.rs
  • crates/mdk-js/src/mdk.js
  • crates/mdk-zig/src/mdk.zig

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

❌ Coverage: 91.09% → 88.66% (-2.43%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/package-mdk-bindings.yml:
- Around line 993-997: The cp line that copies macOS Intel artifact uses the
wrong artifact name `ffi-native-macos-13`; update that command to reference the
correct artifact `ffi-native-macos-15-intel` (i.e., change
`ffi-native-macos-13/libmdk.dylib` to `ffi-native-macos-15-intel/libmdk.dylib`)
so the file copied into `js-package/native/macos-x86_64/` matches the produced
artifact name used in other publish jobs.
- Around line 895-899: The workflow copies a non-existent artifact
"ffi-native-macos-13" — update the third macOS copy line so it references the
correct artifact name used elsewhere ("ffi-native-macos-15-intel") instead of
"ffi-native-macos-13"; modify the cp command that currently targets
ffi-native-macos-13/libmdk.dylib so it points to
ffi-native-macos-15-intel/libmdk.dylib (the other cp lines and artifact names
like ffi-native-macos-latest and ffi-native-macos-13 should not be changed).
- Around line 793-797: The copy step references a nonexistent artifact folder
"ffi-native-macos-13"; update the artifact name to match the build matrix (e.g.,
replace "ffi-native-macos-13" with "ffi-native-macos-15-intel") so the cp
command in the workflow copies from the actual produced artifact (change the
entry that uses ffi-native-macos-13/libmdk.dylib to the matching artifact name
used by the matrix such as ffi-native-macos-15-intel/libmdk.dylib and keep the
destination cbindings-package/lib/macos-x86_64/).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e32287f and bf75c96.

📒 Files selected for processing (1)
  • .github/workflows/package-mdk-bindings.yml

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

❌ Coverage: 91.1% → 88.86% (-2.24%)

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

❌ Coverage: 91.1% → 90.11% (-0.99%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
crates/mdk-js/src/mdk.js (1)

559-573: ⚠️ Potential issue | 🔴 Critical

Avoid unconditional out-buffer cleanup in decryptGroupImage catch path.

At Line 571, out.readAndFree() runs even if argument evaluation throws before the native call executes (for example, f.bufferPtr(data)), which can free unread/uninitialized out pointers.

🔧 Proposed fix
 export function decryptGroupImage(data, expectedHash, key, nonce) {
@@
   const f = ffi();
   const out = f.allocOutBytes();
+  let called = false;
   try {
-    check(f.sym.mdk_decrypt_group_image(
+    const code = f.sym.mdk_decrypt_group_image(
       f.bufferPtr(data), data.length,
       expectedHash ? f.bufferPtr(expectedHash) : f.nullptr,
       expectedHash ? expectedHash.length : 0,
       f.bufferPtr(key), key.length,
       f.bufferPtr(nonce), nonce.length,
       out.ptrPtr, out.lenPtr,
-    ));
+    );
+    called = true;
+    check(code);
     return out.readAndFree();
   } catch (e) {
-    out.readAndFree();
+    if (called) out.readAndFree();
     throw e;
   }
 }
#!/bin/bash
set -euo pipefail

echo "=== Inspect allocOutBytes/readAndFree contracts in backends ==="
rg -n -C4 'allocOutBytes|readAndFree|mdk_bytes_free|ptrPtr|lenPtr' crates/mdk-js/src/ffi_bun.js crates/mdk-js/src/ffi_deno.ts

echo
echo "=== Inspect decryptGroupImage error-path cleanup ==="
sed -n '548,575p' crates/mdk-js/src/mdk.js

Expected verification result: if backend readAndFree() assumes pointers are written by native code, unconditional cleanup on pre-call exceptions is unsafe and must stay guarded.

Based on learnings: MDK handles cryptographic operations so be careful with key material. Review security implications of any changes to cryptographic code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/mdk-js/src/mdk.js` around lines 559 - 573, The catch path
unconditionally calls out.readAndFree() even if argument evaluation (e.g.,
f.bufferPtr(data)) threw before the native call wrote the out pointers; change
decryptGroupImage so the out buffer is only cleaned up when the native call
actually ran and populated the pointers: allocate out as now, wrap only the
native call+readAndFree sequence so that after
check(f.sym.mdk_decrypt_group_image(...)) returns successfully you call
out.readAndFree(), and in the catch block only call out.readAndFree() if a
boolean flag (e.g., nativeCalled or outPopulated) was set immediately after the
native call succeeded; reference symbols: decryptGroupImage, allocOutBytes,
out.readAndFree, f.sym.mdk_decrypt_group_image, f.bufferPtr.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/mdk-js/src/mdk.js`:
- Around line 559-573: The catch path unconditionally calls out.readAndFree()
even if argument evaluation (e.g., f.bufferPtr(data)) threw before the native
call wrote the out pointers; change decryptGroupImage so the out buffer is only
cleaned up when the native call actually ran and populated the pointers:
allocate out as now, wrap only the native call+readAndFree sequence so that
after check(f.sym.mdk_decrypt_group_image(...)) returns successfully you call
out.readAndFree(), and in the catch block only call out.readAndFree() if a
boolean flag (e.g., nativeCalled or outPopulated) was set immediately after the
native call succeeded; reference symbols: decryptGroupImage, allocOutBytes,
out.readAndFree, f.sym.mdk_decrypt_group_image, f.bufferPtr.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf75c96 and ea63107.

📒 Files selected for processing (8)
  • crates/mdk-cbindings/src/error.rs
  • crates/mdk-cbindings/src/groups.rs
  • crates/mdk-cbindings/src/lib.rs
  • crates/mdk-cbindings/src/media.rs
  • crates/mdk-cbindings/src/types.rs
  • crates/mdk-cbindings/src/welcomes.rs
  • crates/mdk-js/src/mdk.js
  • crates/mdk-zig/src/mdk.zig
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/mdk-cbindings/src/error.rs
  • crates/mdk-cbindings/src/welcomes.rs
  • crates/mdk-zig/src/mdk.zig

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

❌ Coverage: 91.1% → 90.11% (-0.99%)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
.github/workflows/package-mdk-bindings.yml (2)

701-714: Consider documenting the OS-to-architecture mapping assumption.

The matrix implicitly assumes macos-latest produces aarch64 binaries. While this is currently correct, adding a brief comment documenting this assumption would help future maintainers. Additionally, Linux ARM64 (aarch64) builds are not included—this may be intentional for initial release but worth tracking if ARM Linux support is desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/package-mdk-bindings.yml around lines 701 - 714, Add a
short inline comment above the build-ffi-native matrix explaining the
OS-to-architecture assumptions (e.g., that macos-latest targets aarch64 while
macos-15-intel targets x86_64) so future maintainers understand the mapping, and
optionally note that Linux ARM64 (aarch64) is not currently included; update the
matrix declaration near the build-ffi-native job and the matrix include entries
(matrix.include, macos-latest, macos-15-intel, ubuntu-latest, windows-latest) to
reflect this comment and/or add a TODO entry for adding an ubuntu-arm64
(aarch64) row if ARM Linux support should be tracked.

773-782: Consider extracting version extraction into a reusable workflow or composite action.

The version extraction logic is duplicated across publish-cbindings, publish-zig, and publish-js jobs. While the current approach is clear and self-contained, extracting this into a composite action would reduce duplication and ensure consistency if the extraction logic needs to change.

Also applies to: 871-880, 972-981

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/package-mdk-bindings.yml around lines 773 - 782, The
"Extract version" step (id: cargo_version) duplicates the same shell extraction
logic across the publish-cbindings, publish-zig, and publish-js jobs; refactor
this by moving the grep/sed extraction into a reusable composite action or a
reusable workflow that exposes the extracted VERSION as an output, then replace
each inlined "Extract version" step with a call to that composite (or uses
workflow_call) so all jobs consume the single source-of-truth output and avoid
duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/package-mdk-bindings.yml:
- Around line 701-714: Add a short inline comment above the build-ffi-native
matrix explaining the OS-to-architecture assumptions (e.g., that macos-latest
targets aarch64 while macos-15-intel targets x86_64) so future maintainers
understand the mapping, and optionally note that Linux ARM64 (aarch64) is not
currently included; update the matrix declaration near the build-ffi-native job
and the matrix include entries (matrix.include, macos-latest, macos-15-intel,
ubuntu-latest, windows-latest) to reflect this comment and/or add a TODO entry
for adding an ubuntu-arm64 (aarch64) row if ARM Linux support should be tracked.
- Around line 773-782: The "Extract version" step (id: cargo_version) duplicates
the same shell extraction logic across the publish-cbindings, publish-zig, and
publish-js jobs; refactor this by moving the grep/sed extraction into a reusable
composite action or a reusable workflow that exposes the extracted VERSION as an
output, then replace each inlined "Extract version" step with a call to that
composite (or uses workflow_call) so all jobs consume the single source-of-truth
output and avoid duplication.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea63107 and b742d47.

📒 Files selected for processing (1)
  • .github/workflows/package-mdk-bindings.yml

@dannym-arx dannym-arx changed the title C and zig bindings because why not... and js because why not C and zig bindings because why not... Mar 4, 2026
@dannym-arx dannym-arx changed the title C and zig bindings because why not... C and zig bindings Mar 4, 2026
@github-actions
Copy link

github-actions bot commented Mar 4, 2026

❌ Coverage: 91.25% → 90.24% (-1.01%)

@dannym-arx dannym-arx removed the request for review from hzrd149 March 4, 2026 20:37
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
@github-actions
Copy link

github-actions bot commented Mar 4, 2026

UniFFI Binding Coverage

7 public method(s) not bound in UniFFI

Coverage: 30/37 bindable methods (81.1%)

⚠️ Not Bound

Method Source
delete_key_package_from_storage crates/mdk-core/src/key_packages.rs:598
delete_key_package_from_storage_by_hash_ref crates/mdk-core/src/key_packages.rs:613
get_ratchet_tree_info crates/mdk-core/src/groups.rs:554
pending_added_members_pubkeys crates/mdk-core/src/groups.rs:610
pending_member_changes crates/mdk-core/src/groups.rs:678
pending_removed_members_pubkeys crates/mdk-core/src/groups.rs:643
prepare_group_image_for_upload_with_options crates/mdk-core/src/extension/group_image.rs:459
✅ Bound methods (30)
Method Source
accept_welcome crates/mdk-core/src/welcomes.rs:315
add_members crates/mdk-core/src/groups.rs:724
clear_pending_commit crates/mdk-core/src/groups.rs:1441
create_group crates/mdk-core/src/groups.rs:1116
create_key_package_for_event crates/mdk-core/src/key_packages.rs:53
create_key_package_for_event_with_options crates/mdk-core/src/key_packages.rs:91
create_message crates/mdk-core/src/messages/create.rs:79
decline_welcome crates/mdk-core/src/welcomes.rs:350
decrypt_group_image crates/mdk-core/src/extension/group_image.rs:227
derive_upload_keypair crates/mdk-core/src/extension/group_image.rs:332
get_group crates/mdk-core/src/groups.rs:474
get_groups crates/mdk-core/src/groups.rs:486
get_last_message crates/mdk-core/src/messages/mod.rs:249
get_members crates/mdk-core/src/groups.rs:520
get_message crates/mdk-core/src/messages/mod.rs:182
get_messages crates/mdk-core/src/messages/mod.rs:221
get_pending_welcomes crates/mdk-core/src/welcomes.rs:72
get_relays crates/mdk-core/src/groups.rs:1062
get_welcome crates/mdk-core/src/welcomes.rs:40
groups_needing_self_update crates/mdk-core/src/groups.rs:504
leave_group crates/mdk-core/src/groups.rs:1388
merge_pending_commit crates/mdk-core/src/groups.rs:1492
parse_key_package crates/mdk-core/src/key_packages.rs:263
prepare_group_image_for_upload crates/mdk-core/src/extension/group_image.rs:413
process_message crates/mdk-core/src/messages/process.rs:301
process_welcome crates/mdk-core/src/welcomes.rs:178
remove_members crates/mdk-core/src/groups.rs:818
self_update crates/mdk-core/src/groups.rs:1287
sync_group_metadata_from_mls crates/mdk-core/src/groups.rs:1563
update_group_data crates/mdk-core/src/groups.rs:995
➖ Intentionally excluded (5)
Method Reason
builder Wrapped by new_mdk(), new_mdk_with_key(), new_mdk_unencrypted() free functions
load_mls_group Only available behind debug-examples feature flag, not for production use
media_manager Returns borrowed reference with lifetime — cannot cross FFI boundary
migrate_group_image_v1_to_v2 Migration utility for internal use only, not needed in client bindings
new Wrapped by new_mdk(), new_mdk_with_key(), new_mdk_unencrypted() free functions

@github-actions
Copy link

github-actions bot commented Mar 4, 2026

❌ Coverage: 91.21% → 90.49% (-0.72%)

@github-actions
Copy link

github-actions bot commented Mar 4, 2026

❌ Coverage: 91.21% → 90.36% (-0.85%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant