Skip to content

feat: extract Rust SDK as five workspace crates#352

Merged
FSM1 merged 51 commits into
mainfrom
feat/phase-23-rust-sdk-extraction
Mar 24, 2026
Merged

feat: extract Rust SDK as five workspace crates#352
FSM1 merged 51 commits into
mainfrom
feat/phase-23-rust-sdk-extraction

Conversation

@FSM1

@FSM1 FSM1 commented Mar 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Extract five Rust crates (cipherbox-crypto, cipherbox-core, cipherbox-api-client, cipherbox-fuse, cipherbox-sdk) mirroring the TypeScript SDK hierarchy under a Cargo workspace
  • Replace all duplicated crypto, domain, and API logic in the desktop app with crate imports — desktop reduced to a thin Tauri shell
  • Add cross-language test vectors (tests/vectors/) with CI parity gate ensuring Rust and TypeScript produce identical crypto output
  • Configure Release Please for all 5 Rust crates with workspace-level version tracking
  • Move Windows WinFsp operations to crates/fuse/src/platform/windows/ (gap closure)
  • Delete ~5,900 lines of orphaned/duplicated desktop code
  • Security hardening: enable zeroize feature on ed25519-dalek, wrap keypair generation in Zeroizing<Vec<u8>>

Crate Architecture

cipherbox-crypto  ← AES-GCM, AES-CTR, ECIES, HKDF, Ed25519
cipherbox-core    ← folder/file/bin metadata, vault blob v2, IPNS records
cipherbox-api-client ← typed HTTP client for CipherBox API
cipherbox-fuse    ← FUSE filesystem (macOS/Linux/Windows platform modules)
cipherbox-sdk     ← sync daemon, upload queue, state management

Requirements Completed

RSDK-01 through RSDK-10 (all 10 Rust SDK Extraction requirements)

Test plan

  • cargo check across all 5 crates (macOS FUSE feature)
  • cargo test -p cipherbox-crypto — 5 cross-language parity tests pass
  • Zero crate::fuse:: references remain in crate-side Windows files
  • Desktop compiles against workspace crates (no duplicated logic)
  • CI: workspace builds on macOS, Linux, Windows
  • CI: cross-language parity gate (scripts/check-vector-parity.sh)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-crate Rust SDK released (crypto, core/domain, typed API client, FUSE, SDK) with cross-language test vectors and parity checks.
  • Chores

    • Desktop rewired into a thin Tauri shell delegating crypto, sync, registry, API, and filesystem logic to new libraries.
    • CI switched to workspace-level Rust builds, added a vector-parity CI job and workspace caching, and added release automation for new crates.
    • VCS now ignores workspace build artifacts (target/).
  • Documentation

    • Added Phase‑23 planning, validation, verification, and security review materials.

FSM1 and others added 30 commits March 24, 2026 07:16
Entire-Checkpoint: 90fd7c9edbbf
Entire-Checkpoint: 72eb8a05c21f
Entire-Checkpoint: 35f549bb020a
Entire-Checkpoint: f4f2df136dd5
- Add derive_vault_key_ipns_keypair to hkdf re-exports in Plans 01, 02
- Fix stale decrypt.rs instruction in Plan 02 (no longer uses detect_blob_version)
- Add vault key HKDF vector requirement to Plan 03 cross-language tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 7a2f7dd1ed36
- Root Cargo.toml workspace with centralized dependency versions
- crates/crypto with 8 modules: aes, aes_ctr, ecies, ed25519, hkdf, ipns_name, utils, error
- Unified CryptoError enum replacing per-module error types
- [patch.crates-io] for vendored fuser at workspace root
- Cargo.lock moved from desktop to workspace root
- target/ added to root .gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4299cb353768
- Delete 6 extracted crypto files (aes, aes_ctr, ecies, ed25519, hkdf, utils)
- Desktop Cargo.toml: add cipherbox-crypto workspace dep, convert shared deps to workspace refs
- Remove [patch.crates-io] from desktop (now at workspace root)
- crypto/mod.rs: re-export cipherbox_crypto modules for backward compatibility
- folder.rs: use CryptoError instead of AesError
- bin.rs: use cipherbox_crypto::ecies directly
- ipns.rs: remove extracted functions, import from cipherbox_crypto
- All 174 desktop tests pass unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9dab54edb87f
- SUMMARY.md with execution results and deviations
- STATE.md updated with position (plan 2 of 7) and decisions
- ROADMAP.md updated with phase 23 progress (1/7 plans)
- REQUIREMENTS.md: RSDK-01 and RSDK-02 marked complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3f1557c66db9
- Add typed HTTP client with auth header injection
- Auth module with login, refresh, logout, vault operations
- IPFS module with fetch, upload, unpin operations
- IPNS module with resolve and publish operations
- Types module with DTOs matching CipherBox API (camelCase serde)
- Error enum with transport, API, auth, and deserialization variants
- No dependency on cipherbox-crypto, tauri, or desktop code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e44d093b4d48
- Add crates/core to workspace with cipherbox-crypto dependency
- Move folder, bin, vault_blob, ipns, registry, decrypt modules
- Create CoreError enum with CryptoError conversion
- File module re-exports FileMetadata types from folder module
- All 12 vault_blob cross-platform tests pass in core crate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 0aefb2f99865
- Create crates/crypto/tests/cross_language.rs with 5 test functions
- Tests load shared vectors from tests/vectors/crypto/*.json (files not yet created)
- Add critical-section std feature for standalone ecies linking
- Add serde dev-dependency to crypto crate for test vector deserialization
- All 5 tests fail: aes_gcm, ed25519, ecies, hkdf, ipns_name (RED phase)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9aa5910b7406
- Delete 6 domain files from desktop (folder, bin, vault_blob, ipns, decrypt, registry types)
- Update crypto/mod.rs to re-export domain modules from cipherbox-core
- Update fuse modules to use crate::crypto::decrypt:: instead of crate::fuse::decrypt::
- Update registry/mod.rs to import types from cipherbox_core::registry
- Move prost/ciborium to dev-dependencies (only used in tests for CBOR decoding)
- All 162 desktop tests pass with domain logic in external crate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c506e2d0b4ed
- Add 23-02-SUMMARY.md with execution results
- Update STATE.md with plan position and decisions
- Update ROADMAP.md progress for phase 23
- Mark RSDK-04 requirement complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c9f41d003cf2
- Create 23-03-SUMMARY.md with execution results
- Update STATE.md: advance to plan 4/7, add 3 key decisions, metrics
- Update ROADMAP.md: 3/7 plans complete for phase 23
- Mark RSDK-03 and RSDK-05 requirements complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3ef4f6c027e3
…registry

- Add cipherbox-sdk crate with SyncDaemon (generic status callback, no Tauri dep),
  WriteQueue (FIFO offline write queue), KeyState (zeroizable key material),
  DeviceRegistry operations, CipherBoxSdkClient top-level orchestrator
- Add crates/fuse/src/lib.rs stub to unblock workspace compilation (from 23-04)
- SyncDaemon uses Arc<dyn Fn(SyncStatus)> callback instead of tauri::AppHandle
- Registry accepts DeviceInfo parameter instead of using keyring directly
- cargo check -p cipherbox-sdk passes with zero warnings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 28733e1f8be8
- AppState wraps SDK KeyState (pub sdk: Arc<KeyState>) instead of owning
  key material fields directly; clear_keys() delegates to sdk.clear()
- Desktop sync/mod.rs is now a thin bridge: creates SDK SyncDaemon with
  a Tauri tray status callback that maps SyncStatus to TrayStatus
- Deleted sync/queue.rs and sync/tests.rs (logic moved to SDK crate)
- Registry/mod.rs delegates to cipherbox_sdk::registry::register_device,
  keeping only OS-specific device ID retrieval (keyring) local
- api/client.rs re-exports cipherbox_api_client::ApiClient (type alias)
- api/types.rs re-exports TeeKeysResponse from cipherbox_api_client
- All state.api/private_key/etc refs changed to state.sdk.api/etc
- Added cipherbox-api-client + cipherbox-sdk to desktop Cargo.toml
- Removed hostname dep from desktop (now in SDK crate)
- 152 tests pass, cargo check exits 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 79fbe1440bca
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 45e7247ef20c
- Add operations.rs with Filesystem trait impl dispatching to sub-modules
- Add read_ops.rs (init, destroy, lookup, getattr, open, read, release)
- Add write_ops.rs (setattr, write, create, unlink, mkdir, rmdir, rename)
- Add dir_ops.rs (readdir, opendir, releasedir, statfs)
- Add platform/macos.rs (FUSE-T unmount with diskutil force fallback)
- Add platform/linux.rs (fusermount3/fusermount/umount chain)
- Make PublishQueueEntry and publish_queue field public for cross-crate init
- Desktop fuse/mod.rs rewritten as thin bridge: re-exports from crate,
  keeps only mount_filesystem (needs AppState) and unmount delegates
- Desktop Cargo.toml adds cipherbox-fuse dep with feature propagation
- All API calls use cipherbox_api_client instead of desktop api module
- All crypto calls use cipherbox_crypto/cipherbox_core instead of crate::crypto
- cargo check -p cipherbox-fuse --features fuse exits 0
- cargo check -p cipherbox-desktop --features fuse exits 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6fb87319e30f
- Add 23-04-SUMMARY.md with extraction details and decisions
- Update STATE.md with plan progress and metrics
- Update ROADMAP.md with Phase 23 plan count (5/7 complete)
- Mark RSDK-06 requirement as complete in REQUIREMENTS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bc3d4d4bd6c
…ty gate

- Migrate cargo check/test to --workspace on all three platforms (Windows, macOS, Linux)
- Update cache keys to reference root Cargo.lock instead of desktop-specific lockfile
- Add crates/**, Cargo.toml, Cargo.lock, tests/vectors/** to desktop path filters
- Add vector-parity CI job that runs Rust and TS vector tests then validates parity
- Create scripts/check-vector-parity.sh for vector file existence and JSON validation
- Update desktop-e2e.yml to use workspace-level cargo build and root target directory
- Remove stale apps/desktop/src-tauri/Cargo.lock from path filters (workspace uses root lockfile)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3989b0a5592b
- Add 5 Rust crate entries (crypto, core, api-client, fuse, sdk) with release-type: rust
- Set include-component-in-tag: true and bump-minor-pre-major: true for all crates
- Add crate Cargo.toml paths to root package extra-files for unified version propagation
- Add initial 0.1.0 versions to .release-please-manifest.json for all crates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e7e375a283c2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b9d7d74c166c
- Remove unused imports in desktop: Manager, DeviceAuthStatus, DeviceEntry,
  DevicePlatform, DeviceRegistry, QueuedWrite, UploadHandler, WriteQueue
- Prefix unused variable _webview in main.rs
- Prefix unused variable _resolved in crates/fuse/src/inode.rs
- Add #[allow(dead_code)] to cipherbox-fuse operations module (extracted
  functions not yet consumed by desktop)
- Add #[allow(unused_imports)] to fuse/mod.rs re-exports (consumed by
  desktop submodules via crate::fuse:: paths, not detectable by compiler)
- Remove redundant re-exports: cache, operations, read_ops, write_ops,
  dir_ops, spawn_metadata_publish from fuse bridge module
- All workspace tests pass, no warnings outside vendored fuser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 69c3abefc339
- Create 23-06-SUMMARY.md documenting desktop cleanup verification
- Update STATE.md with metrics, decisions, and session info
- Update ROADMAP.md progress (7/7 plans complete for phase 23)
- Mark RSDK-08 requirement complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 648f8ea8ce29
…y issues

Remove 9 orphaned .rs files from desktop fuse/ that were never compiled
(no mod declarations) after extraction to cipherbox-fuse crate. Fix
misleading #[allow(dead_code)] on operations::implementation module that
IS used by read_ops/write_ops/dir_ops. Consolidate double HashMap lookup
in read_ops::handle_read to single lookup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d0aee4a092c
- Create crates/fuse/src/platform/windows/ with mod.rs, operations.rs,
  read_ops.rs, write_ops.rs, dir_ops.rs
- Rewrite all imports from crate::fuse::* to crate::* for crate context
- Make block_with_timeout public in lib.rs for cross-module access
- Use super:: for intra-module references (read_ops, write_ops, dir_ops)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 352fc8fdda86
- Delete 4 operation files from apps/desktop/src-tauri/src/fuse/windows/
  (operations.rs, read_ops.rs, write_ops.rs, dir_ops.rs)
- Remove submodule declarations (mod read_ops, mod write_ops, etc.)
- Update WinFspContext reference to cipherbox_fuse::platform::windows
- Desktop fuse/windows/ now contains only mount/unmount logic (mod.rs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c9872cdbd1f2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e623db227223

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (1)
crates/api-client/src/client.rs (1)

58-69: ⚠️ Potential issue | 🟠 Major

Drop the token read lock before awaiting the request.

Each helper (authenticated_get, authenticated_post, authenticated_multipart_post) holds the RwLockReadGuard across .send().await, blocking concurrent set_access_token/clear_access_token calls for the entire request duration. Clone the token before the await and drop the guard immediately.

♻️ Suggested fix
-        let token = self.access_token.read().await;
+        let token = self.access_token.read().await.clone();
@@
-        if let Some(ref t) = *token {
-            builder = builder.bearer_auth(t);
+        if let Some(t) = token.as_deref() {
+            builder = builder.bearer_auth(t);
         }

Apply to authenticated_post (lines 79–91) and authenticated_multipart_post (lines 125–137).

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

In `@crates/api-client/src/client.rs` around lines 58 - 69, The helpers
authenticated_get, authenticated_post, and authenticated_multipart_post
currently hold the RwLockReadGuard (self.access_token.read().await) across
builder.send().await which blocks set_access_token/clear_access_token; change
each to clone the token out of the guard (e.g., let token_opt = (*token).clone()
or token.clone()), then drop the guard immediately (allow it to go out of scope)
before building/sending the request so the await happens without holding the
read lock and concurrent writers can proceed.
🧹 Nitpick comments (3)
.planning/security/REVIEW-2026-03-24-phase23.md (1)

10-10: Resolve internal contradiction in the report’s key-material handling claims.

The executive summary says key material is wrapped in Zeroizing<Vec<u8>> throughout, while HIGH-2 asserts a non-zeroizing private key return. Keep one consistent position based on the current implementation.

Also applies to: 69-89

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

In @.planning/security/REVIEW-2026-03-24-phase23.md at line 10, The report
contains a contradiction between the executive summary claiming key material is
wrapped in Zeroizing<Vec<u8>> throughout and HIGH-2 claiming a non-zeroizing
private key return; inspect the actual implementation to confirm which is true,
then make the text consistent: either (A) if the code returns a non-zeroizing
private key, update the executive summary and any mentions of "throughout" to
qualify where Zeroizing is used and document the specific function/path that
leaks (refer to HIGH-2 and occurrences of "Zeroizing<Vec<u8>>"), or (B) if the
code already zeroizes everywhere, revise HIGH-2 to remove the false positive and
note the correct locations using Zeroizing; update all other instances in the
detailed findings (the section currently around the HIGH-2 discussion and the
executive summary) so both places say the same thing.
crates/crypto/src/ipns_name.rs (1)

13-25: Consider validating or constraining the public key length.

The function is pub and accepts &[u8], but line 22 casts len() to u8, which would silently truncate lengths > 255. While derive_ipns_name correctly constrains to 32 bytes via &[u8; 32], direct callers of this public helper could produce malformed protobuf.

🛡️ Suggested defensive fix
 pub fn encode_libp2p_public_key(ed25519_public_key: &[u8]) -> Vec<u8> {
+    debug_assert!(
+        ed25519_public_key.len() <= 255,
+        "Public key too long for single-byte length encoding"
+    );
     let mut buf = Vec::new();
     // Field 1 (Type): varint, field_number=1, wire_type=0 => tag = 0x08
     buf.push(0x08);

Alternatively, change the signature to &[u8; 32] to match Ed25519 key size.

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

In `@crates/crypto/src/ipns_name.rs` around lines 13 - 25, The function
encode_libp2p_public_key currently casts ed25519_public_key.len() to u8 which
can truncate inputs >255; fix by enforcing Ed25519 key length: either change the
signature to accept &[u8; 32] or validate at the start of
encode_libp2p_public_key that ed25519_public_key.len() == 32 and return a
Result<Vec<u8>, Error> (or panic) on mismatch; perform the length check before
pushing the length byte and only cast to u8 after the check to avoid silent
truncation.
crates/core/src/decrypt.rs (1)

10-50: Keep the new core boundary typed; don’t return String errors.

These helpers are public cipherbox-core APIs, but they collapse JSON/base64/length/crypto failures into free-form Strings. That makes downstream handling ad hoc and hardens an unstructured error contract right as the crate is being extracted. Please map these cases into CoreError variants instead.

Also applies to: 54-89

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

In `@crates/core/src/decrypt.rs` around lines 10 - 50, Change the function
signature decrypt_metadata_from_ipfs_public (and the other helper in the 54-89
range) to return Result<FolderMetadata, CoreError> instead of Result<...,
String>, and map every failure path (serde_json::from_slice, hex::decode, IV
length check, base64 decode, folder_key try_into, and
crate::folder::decrypt_folder_metadata) into specific CoreError variants (e.g.,
ParseError, InvalidHex/InvalidIv, Base64Error, InvalidKeyLength, CryptoError)
instead of formatting free-form Strings; update error mapping calls to construct
the appropriate CoreError (preserving underlying error details where possible)
so callers receive structured CoreError variants rather than plain strings.
🤖 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/ci.yml:
- Around line 669-674: The vector-parity job currently lists test in its needs,
which causes the job to be skipped when test is skipped; remove 'test' from the
needs array for the vector-parity job (the symbol to edit is the vector-parity
job's needs: entry) so it no longer depends on the test job, and if you want to
ensure the job still runs even when other jobs fail/are cancelled replace the
current if condition starts-with '!failure() && !cancelled()' with an always()
wrapper combined with your existing vector-change checks (e.g., if: always() &&
(needs.changes.outputs.desktop == 'true' || needs.changes.outputs.src ==
'true')).

In `@apps/desktop/src-tauri/src/fuse/mod.rs`:
- Around line 254-257: In the test module (mod tests) the import list includes
an unused symbol FolderEntry; remove FolderEntry from the use statement that
currently reads use cipherbox_core::folder::{FilePointer, FolderChild,
FolderEntry, FolderMetadata}; so the tests only import FilePointer, FolderChild,
and FolderMetadata to eliminate the unused import warning.

In `@crates/api-client/src/auth.rs`:
- Around line 58-67: The logout function currently uses the ? operator on
client.authenticated_post which lets transport errors bubble up; change logout
to await client.authenticated_post("/auth/logout", &()) into a Result (e.g., let
resp_result = client.authenticated_post(...).await) and handle both Err and Ok:
on Err log a warning (include the error) and continue to return Ok(()) so local
state can be cleared, and on Ok inspect resp.status() as before and warn on
non-success; reference the logout function and authenticated_post call when
making this change.

In `@crates/api-client/src/client.rs`:
- Around line 112-115: In get_bytes, currently any HTTP response is treated as
success; modify get_bytes to check resp.status().is_success() after awaiting
send() and before reading bytes, and if not successful return
ApiError::ApiResponse (include the response status and body/text for context)
instead of returning the raw bytes; reference the resp variable and the
get_bytes method so the change mirrors the pattern used in auth.rs, ipfs.rs, and
ipns.rs where non-success statuses are converted to ApiError::ApiResponse.

In `@crates/api-client/src/types.rs`:
- Around line 139-161: The IpnsPublishRequest currently derives Debug and may
leak sensitive fields `record` and `encrypted_ipns_private_key`; replace the
derived Debug on the struct with a manual impl or use the same redaction pattern
as the auth DTOs: remove `#[derive(Debug, Serialize)]` (or at least remove
Debug), implement Debug for `IpnsPublishRequest` to redact `record` and
`encrypted_ipns_private_key` (show placeholders like "<redacted>" or only
lengths), keep Serialize and serde attrs intact, and ensure `key_epoch`,
`expected_sequence_number`, `ipns_name`, and `metadata_cid` are represented
normally in the Debug output; update any imports or traits accordingly.

In `@crates/crypto/src/ecies.rs`:
- Around line 14-15: Update the ECIES constant and its doc comment: change the
doc comment describing the output format to "ephemeral_pubkey(65) || nonce(12)
|| tag(16) || ciphertext" (nonce is 12 bytes for AES-GCM), and change the
ECIES_MIN_CIPHERTEXT_SIZE constant calculation (symbol
ECIES_MIN_CIPHERTEXT_SIZE) to include the 12-byte nonce plus 16-byte auth tag by
using SECP256K1_PUBLIC_KEY_SIZE + 12 + 16 so the minimum ciphertext size for
empty plaintext is 93 bytes.

In `@crates/fuse/src/lib.rs`:
- Around line 617-642: The drain_refresh_completions() logic is performing
network I/O synchronously (using block_with_timeout and calling
cipherbox_api_client::ipns::resolve_ipns /
cipherbox_api_client::ipfs::fetch_content) while draining the refresh channel;
instead, enqueue each unresolved pointer returned by
self.inodes.get_unresolved_file_pointers() onto the existing background
worker/channel (the same path used by release() or the background resolver)
including ino, fp_ipns and the folder_key, then return immediately from
drain_refresh_completions(); have the background worker perform resolve_ipns and
fetch_content (using rt/api as before), decrypt with
cipherbox_core::decrypt_file_metadata_from_ipfs_public, and call
self.inodes.resolve_file_pointer(...) when each resolution completes so the FUSE
thread never blocks on network timeouts.
- Around line 235-243: The folder/IPNS key material is being copied into plain
Vec<u8> and passed into spawn_metadata_publish, leaving unsanitized heap copies;
change the types and flows to use zeroize::Zeroizing (e.g., Zeroizing<Vec<u8>>)
instead of plain Vec<u8> so keys are zeroized when dropped, update
build_folder_metadata to produce/return Zeroizing<Vec<u8>> for folder_key and
ipns_private_key (avoid intermediate plain Vec copies), change
spawn_metadata_publish signature to accept Zeroizing<Vec<u8>> for folder_key and
ipns_private_key and propagate those types through the background task, and fix
the other occurrences you noted (around the 491-503 area) to use Zeroizing as
well; also add the appropriate use/import for zeroize::Zeroizing.
- Around line 131-153: The resolve_sequence function currently returns Err when
cipherbox_api_client::ipns::resolve_ipns(...) returns an error and there is no
cached sequence, which prevents first-time publishes; change resolve_sequence so
that if resolve_ipns fails with a "not found"/missing-record error (or any error
indicating no IPNS record) and get_cached(ipns_name) is None, it treats the
sequence as 0: update_cache(ipns_name, 0) and return Ok(0) instead of Err; keep
the existing behavior of using cached value when present and logging the warning
as before.

In `@crates/fuse/src/operations.rs`:
- Around line 162-178: The per-file IPNS publish flow currently treats
PublishResult::Conflict as success by always calling
coordinator.record_publish(file_ipns_name, new_seq) and logging success;
instead, only record and log on cipherbox_api_client::PublishResult::Success.
Update the match over cipherbox_api_client::ipns::publish_ipns(...) so that
coordinator.record_publish(...) and the "Per-file IPNS publish succeeded" log
are executed inside the Success arm, and in the Conflict { .. } arm do not
advance the local sequence cache—return or propagate an error (or handle it
explicitly) so callers know the publish failed; reference the
IpnsPublishRequest, publish_ipns, PublishResult::Success,
PublishResult::Conflict, coordinator.record_publish, file_ipns_name, and new_seq
to find the code to change.

In `@crates/fuse/src/platform/linux.rs`:
- Around line 12-17: The temp directory cleanup currently happens before unmount
and can remove in-use files; move the remove_dir_all(&temp_dir) block so it runs
only after a successful unmount operation. Locate the variable temp_dir and the
existing unmount call (the function or code that performs the filesystem
unmount) and transfer the logic that checks temp_dir.exists() and calls
std::fs::remove_dir_all(&temp_dir) into the cleanup path executed after unmount
succeeds, preserving the log::warn! on Err(e) to report cleanup failures. Ensure
no early return prevents running this post-unmount cleanup and that errors
during unmount are handled before attempting removal.
- Line 19: The code uses mount_path.to_str().unwrap() which can panic on
non-UTF-8 paths; replace this unsafe unwrap in the block that defines mount_str
by handling the Option properly — either use mount_path.to_string_lossy() to get
a safe String/Cow<str> or propagate/return an error when to_str() is None so
non-UTF-8 mounts are handled gracefully; update all uses of mount_str
accordingly (look for the let mount_str = mount_path.to_str().unwrap() binding
and callers that expect &str).

In `@crates/fuse/src/platform/macos.rs`:
- Around line 20-22: Replace uses of mount_path.to_str().unwrap() to avoid
panics on non-UTF-8 paths: pass the Path/PathBuf directly to Command.arg (e.g.
.arg(&mount_path) or .arg(mount_path.as_os_str())) in the macOS unmount calls
found in this file (the Command::new("umount") invocation and the other similar
invocation around lines 30-33). This removes the to_str().unwrap() panic risk
while preserving correct OS-string argument handling.

---

Outside diff comments:
In `@crates/api-client/src/client.rs`:
- Around line 58-69: The helpers authenticated_get, authenticated_post, and
authenticated_multipart_post currently hold the RwLockReadGuard
(self.access_token.read().await) across builder.send().await which blocks
set_access_token/clear_access_token; change each to clone the token out of the
guard (e.g., let token_opt = (*token).clone() or token.clone()), then drop the
guard immediately (allow it to go out of scope) before building/sending the
request so the await happens without holding the read lock and concurrent
writers can proceed.

---

Nitpick comments:
In @.planning/security/REVIEW-2026-03-24-phase23.md:
- Line 10: The report contains a contradiction between the executive summary
claiming key material is wrapped in Zeroizing<Vec<u8>> throughout and HIGH-2
claiming a non-zeroizing private key return; inspect the actual implementation
to confirm which is true, then make the text consistent: either (A) if the code
returns a non-zeroizing private key, update the executive summary and any
mentions of "throughout" to qualify where Zeroizing is used and document the
specific function/path that leaks (refer to HIGH-2 and occurrences of
"Zeroizing<Vec<u8>>"), or (B) if the code already zeroizes everywhere, revise
HIGH-2 to remove the false positive and note the correct locations using
Zeroizing; update all other instances in the detailed findings (the section
currently around the HIGH-2 discussion and the executive summary) so both places
say the same thing.

In `@crates/core/src/decrypt.rs`:
- Around line 10-50: Change the function signature
decrypt_metadata_from_ipfs_public (and the other helper in the 54-89 range) to
return Result<FolderMetadata, CoreError> instead of Result<..., String>, and map
every failure path (serde_json::from_slice, hex::decode, IV length check, base64
decode, folder_key try_into, and crate::folder::decrypt_folder_metadata) into
specific CoreError variants (e.g., ParseError, InvalidHex/InvalidIv,
Base64Error, InvalidKeyLength, CryptoError) instead of formatting free-form
Strings; update error mapping calls to construct the appropriate CoreError
(preserving underlying error details where possible) so callers receive
structured CoreError variants rather than plain strings.

In `@crates/crypto/src/ipns_name.rs`:
- Around line 13-25: The function encode_libp2p_public_key currently casts
ed25519_public_key.len() to u8 which can truncate inputs >255; fix by enforcing
Ed25519 key length: either change the signature to accept &[u8; 32] or validate
at the start of encode_libp2p_public_key that ed25519_public_key.len() == 32 and
return a Result<Vec<u8>, Error> (or panic) on mismatch; perform the length check
before pushing the length byte and only cast to u8 after the check to avoid
silent truncation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 286a5979-8784-453e-8f15-dafa308750ab

📥 Commits

Reviewing files that changed from the base of the PR and between ffa8d25 and 9a85aad.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • packages/api-client/CHANGELOG.md is excluded by !packages/api-client/**
📒 Files selected for processing (128)
  • .github/workflows/ci.yml
  • .github/workflows/desktop-e2e.yml
  • .gitignore
  • .planning/REQUIREMENTS.md
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/23-rust-sdk-extraction/23-01-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-01-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-02-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-02-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-03-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-03-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-04-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-04-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-05-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-05-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-06-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-06-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-07-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-07-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-08-PLAN.md
  • .planning/phases/23-rust-sdk-extraction/23-08-SUMMARY.md
  • .planning/phases/23-rust-sdk-extraction/23-CONTEXT.md
  • .planning/phases/23-rust-sdk-extraction/23-RESEARCH.md
  • .planning/phases/23-rust-sdk-extraction/23-VALIDATION.md
  • .planning/phases/23-rust-sdk-extraction/23-VERIFICATION.md
  • .planning/security/REVIEW-2026-03-24-phase23.md
  • .release-please-manifest.json
  • CHANGELOG.md
  • Cargo.toml
  • apps/desktop/src-tauri/Cargo.toml
  • apps/desktop/src-tauri/src/api/ipns.rs
  • apps/desktop/src-tauri/src/api/mod.rs
  • apps/desktop/src-tauri/src/commands/auth.rs
  • apps/desktop/src-tauri/src/commands/sync.rs
  • apps/desktop/src-tauri/src/commands/vault.rs
  • apps/desktop/src-tauri/src/crypto/aes.rs
  • apps/desktop/src-tauri/src/crypto/bin.rs
  • apps/desktop/src-tauri/src/crypto/ecies.rs
  • apps/desktop/src-tauri/src/crypto/ed25519.rs
  • apps/desktop/src-tauri/src/crypto/mod.rs
  • apps/desktop/src-tauri/src/crypto/tests.rs
  • apps/desktop/src-tauri/src/fuse/decrypt.rs
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • apps/desktop/src-tauri/src/fuse/operations.rs
  • apps/desktop/src-tauri/src/fuse/windows/mod.rs
  • apps/desktop/src-tauri/src/keychain.rs
  • apps/desktop/src-tauri/src/main.rs
  • apps/desktop/src-tauri/src/registry/mod.rs
  • apps/desktop/src-tauri/src/registry/types.rs
  • apps/desktop/src-tauri/src/state.rs
  • apps/desktop/src-tauri/src/sync/mod.rs
  • apps/desktop/src-tauri/src/sync/tests.rs
  • apps/desktop/src-tauri/src/tray/mod.rs
  • apps/desktop/src-tauri/tauri.conf.json
  • apps/desktop/src-tauri/vendor/fuser/build.rs
  • crates/api-client/Cargo.toml
  • crates/api-client/src/auth.rs
  • crates/api-client/src/client.rs
  • crates/api-client/src/error.rs
  • crates/api-client/src/ipfs.rs
  • crates/api-client/src/ipns.rs
  • crates/api-client/src/lib.rs
  • crates/api-client/src/types.rs
  • crates/core/Cargo.toml
  • crates/core/src/bin.rs
  • crates/core/src/decrypt.rs
  • crates/core/src/error.rs
  • crates/core/src/file.rs
  • crates/core/src/folder.rs
  • crates/core/src/ipns.rs
  • crates/core/src/lib.rs
  • crates/core/src/registry.rs
  • crates/core/src/vault_blob.rs
  • crates/crypto/Cargo.toml
  • crates/crypto/src/aes.rs
  • crates/crypto/src/aes_ctr.rs
  • crates/crypto/src/ecies.rs
  • crates/crypto/src/ed25519.rs
  • crates/crypto/src/error.rs
  • crates/crypto/src/hkdf.rs
  • crates/crypto/src/ipns_name.rs
  • crates/crypto/src/lib.rs
  • crates/crypto/src/utils.rs
  • crates/crypto/tests/cross_language.rs
  • crates/fuse/Cargo.toml
  • crates/fuse/src/cache.rs
  • crates/fuse/src/constants.rs
  • crates/fuse/src/dir_ops.rs
  • crates/fuse/src/error.rs
  • crates/fuse/src/file_handle.rs
  • crates/fuse/src/helpers.rs
  • crates/fuse/src/inode.rs
  • crates/fuse/src/lib.rs
  • crates/fuse/src/operations.rs
  • crates/fuse/src/platform/linux.rs
  • crates/fuse/src/platform/macos.rs
  • crates/fuse/src/platform/mod.rs
  • crates/fuse/src/platform/windows/dir_ops.rs
  • crates/fuse/src/platform/windows/mod.rs
  • crates/fuse/src/platform/windows/operations.rs
  • crates/fuse/src/platform/windows/read_ops.rs
  • crates/fuse/src/platform/windows/write_ops.rs
  • crates/fuse/src/read_ops.rs
  • crates/fuse/src/write_ops.rs
  • crates/sdk/Cargo.toml
  • crates/sdk/src/client.rs
  • crates/sdk/src/error.rs
  • crates/sdk/src/lib.rs
  • crates/sdk/src/queue.rs
  • crates/sdk/src/registry.rs
  • crates/sdk/src/state.rs
  • crates/sdk/src/sync.rs
  • packages/core/CHANGELOG.md
  • packages/crypto/CHANGELOG.md
  • packages/sdk-core/CHANGELOG.md
  • packages/sdk/CHANGELOG.md
  • release-please-config.json
  • scripts/check-vector-parity.sh
  • tests/vectors/core/bin-metadata.json
  • tests/vectors/core/folder-metadata.json
  • tests/vectors/core/ipns-record.json
  • tests/vectors/core/vault-blob.json
  • tests/vectors/crypto/aes-gcm.json
  • tests/vectors/crypto/ecies.json
  • tests/vectors/crypto/ed25519.json
  • tests/vectors/crypto/hkdf.json
  • tests/vectors/crypto/ipns-name.json
💤 Files with no reviewable changes (12)
  • apps/desktop/src-tauri/src/crypto/ed25519.rs
  • apps/desktop/src-tauri/src/api/mod.rs
  • apps/desktop/src-tauri/src/crypto/bin.rs
  • apps/desktop/src-tauri/src/registry/types.rs
  • apps/desktop/src-tauri/src/api/ipns.rs
  • apps/desktop/src-tauri/src/crypto/ecies.rs
  • apps/desktop/src-tauri/src/crypto/mod.rs
  • apps/desktop/src-tauri/src/crypto/aes.rs
  • apps/desktop/src-tauri/src/fuse/decrypt.rs
  • apps/desktop/src-tauri/src/sync/tests.rs
  • apps/desktop/src-tauri/src/crypto/tests.rs
  • apps/desktop/src-tauri/src/fuse/operations.rs

Comment thread .github/workflows/ci.yml
Comment thread apps/desktop/src-tauri/src/fuse/mod.rs
Comment thread crates/api-client/src/auth.rs
Comment thread crates/api-client/src/client.rs Outdated
Comment thread crates/api-client/src/types.rs
Comment thread crates/fuse/src/lib.rs
Comment thread crates/fuse/src/operations.rs
Comment thread crates/fuse/src/platform/linux.rs Outdated
Comment thread crates/fuse/src/platform/linux.rs Outdated
Comment thread crates/fuse/src/platform/macos.rs
- Pass Path directly to Command::arg in macOS/Linux unmount (avoid
  to_str().unwrap() panic on non-UTF-8 paths)
- Use ApiError::RequestFailed instead of DeserializationFailed for
  response body read and multipart construction errors in IPFS client
- Mark 23-VALIDATION.md as complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b6f250f5b446
@FSM1 FSM1 requested a review from Copilot March 24, 2026 19:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 98 out of 130 changed files in this pull request and generated 6 comments.

Comment thread crates/crypto/src/ecies.rs
Comment thread crates/crypto/src/ipns_name.rs Outdated
Comment thread crates/crypto/src/ipns_name.rs
Comment thread crates/api-client/src/error.rs
Comment thread crates/core/src/bin.rs Outdated
Comment thread crates/core/src/registry.rs
FSM1 and others added 2 commits March 24, 2026 20:09
- Remove unused FolderEntry import in desktop fuse/mod.rs tests
- Make logout truly best-effort (catch transport errors instead of ?)
- Add custom Debug for IpnsPublishRequest that redacts key material
- Remove dead get_bytes method from ApiClient
- Move temp directory cleanup after unmount on macOS and Linux

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 06b9488355c7
- Validate Ed25519 key length in encode_libp2p_public_key (reject
  non-32-byte keys instead of silent truncation via as u8)
- Fix doc comment in bin.rs (mirrors @cipherbox/core, not crypto)
- Rename test to match actual behavior (null, not skipped)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 81a4389b15e3

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crates/api-client/src/types.rs (1)

4-4: Broaden the module doc wording to match current scope.

The comment currently says only auth DTOs use manual redaction, but this file now includes redacted Debug for IPNS publish payloads too.

✏️ Suggested wording tweak
-//! Auth DTOs use manual Debug impls to redact sensitive fields.
+//! DTOs that may carry sensitive fields use manual Debug impls to redact them.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/api-client/src/types.rs` at line 4, Update the module doc comment in
types.rs to reflect that manual Debug implementations are used to redact
sensitive fields beyond just auth DTOs—specifically mention that IPNS publish
payloads (and any other sensitive DTOs in this file) also use redacted Debug
impls; locate the module-level doc comment near the top of types.rs and change
the wording to cover both auth DTOs and IPNS publish payloads (and similar
types) so it accurately describes the scope of the redacted Debug
implementations.
apps/desktop/src-tauri/src/fuse/mod.rs (1)

127-142: Consider extracting FilePointer resolution into a helper.

The resolution logic here (lines 127-142) is nearly identical to lines 161-174. A small helper function would reduce duplication and make future maintenance easier:

async fn resolve_file_pointers(
    inodes: &mut InodeTable,
    api: &ApiClient,
    folder_key: &[u8; 32],
) { /* ... */ }

This is a minor code-quality improvement that could be addressed in a follow-up.

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

In `@apps/desktop/src-tauri/src/fuse/mod.rs` around lines 127 - 142, Extract the
duplicated FilePointer resolution block into a single async helper (e.g., async
fn resolve_file_pointers(inodes: &mut InodeTable, api: &ApiClient,
root_folder_key: &[u8;32]) ) and replace both occurrences with calls to it; the
helper should call inodes.get_unresolved_file_pointers(), loop over (fp_ino,
fp_ipns), use cipherbox_api_client::ipns::resolve_ipns and
cipherbox_api_client::ipfs::fetch_content to obtain enc_bytes, decrypt with
cipherbox_core::decrypt_file_metadata_from_ipfs_public using the provided folder
key, then call inodes.resolve_file_pointer(...) with the decrypted metadata,
handling errors by skipping unresolved entries as the current code does.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src-tauri/src/fuse/mod.rs`:
- Around line 127-142: Extract the duplicated FilePointer resolution block into
a single async helper (e.g., async fn resolve_file_pointers(inodes: &mut
InodeTable, api: &ApiClient, root_folder_key: &[u8;32]) ) and replace both
occurrences with calls to it; the helper should call
inodes.get_unresolved_file_pointers(), loop over (fp_ino, fp_ipns), use
cipherbox_api_client::ipns::resolve_ipns and
cipherbox_api_client::ipfs::fetch_content to obtain enc_bytes, decrypt with
cipherbox_core::decrypt_file_metadata_from_ipfs_public using the provided folder
key, then call inodes.resolve_file_pointer(...) with the decrypted metadata,
handling errors by skipping unresolved entries as the current code does.

In `@crates/api-client/src/types.rs`:
- Line 4: Update the module doc comment in types.rs to reflect that manual Debug
implementations are used to redact sensitive fields beyond just auth
DTOs—specifically mention that IPNS publish payloads (and any other sensitive
DTOs in this file) also use redacted Debug impls; locate the module-level doc
comment near the top of types.rs and change the wording to cover both auth DTOs
and IPNS publish payloads (and similar types) so it accurately describes the
scope of the redacted Debug implementations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3154fbb-ff87-476a-a812-3cde881811e8

📥 Commits

Reviewing files that changed from the base of the PR and between 50187b2 and 6ae9e78.

📒 Files selected for processing (6)
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • crates/api-client/src/auth.rs
  • crates/api-client/src/client.rs
  • crates/api-client/src/types.rs
  • crates/fuse/src/platform/linux.rs
  • crates/fuse/src/platform/macos.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/fuse/src/platform/linux.rs
  • crates/fuse/src/platform/macos.rs
  • crates/api-client/src/auth.rs

…S before parity tests

- Add default-features = false to desktop's cipherbox-fuse dependency
  so the fuse feature (which pulls in fuser/unix-only code) isn't
  activated through the dependency chain on Windows
- Add pnpm build step for @cipherbox/core and @cipherbox/crypto before
  running TS vector tests (vault-ipns.test.ts imports @cipherbox/core)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9e4f0b1c521c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/core/src/registry.rs (1)

144-144: Remove dead branch in assertion.

The field is named status in the struct, and with camelCase renaming it remains "status" in JSON (no underscore to convert). The "authStatus" branch will never match and is dead code.

🧹 Proposed fix
-        assert!(json.contains("\"authStatus\"") || json.contains("\"status\""));
+        assert!(json.contains("\"status\""));
+        assert!(!json.contains("\"authStatus\""));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/core/src/registry.rs` at line 144, The assertion at the end of the
test currently checks for either "\"authStatus\"" or "\"status\"" but
"authStatus" is impossible due to the struct field being named `status` (and
serde camelCase keeps it as "status"), so remove the dead branch and change the
assertion to only verify that the serialized JSON contains "\"status\"" (update
the assert! that currently references both variants in the file containing the
test/assertion).
crates/core/src/ipns.rs (1)

18-20: Clarify the legacy path in the re-export comment

The comment on Line 19 references crate::crypto::ipns::derive_ipns_name, which may mislead if the active public path is now crate::ipns::derive_ipns_name. Please update the comment to the exact current path(s).

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

In `@crates/core/src/ipns.rs` around lines 18 - 20, Update the comment above the
re-export of derive_ipns_name to reference the correct current public path(s):
mention that derive_ipns_name is re-exported for backward compatibility so
code/tests referencing the old path (crate::crypto::ipns::derive_ipns_name) and
the current path (crate::ipns::derive_ipns_name) both continue to work; adjust
the comment to explicitly list these exact paths and keep the note about
backward compatibility next to the pub use of
cipherbox_crypto::ipns_name::derive_ipns_name.
🤖 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/core/src/ipns.rs`:
- Around line 398-410: The test different_sequences_produce_different_output is
flaky because create_ipns_record uses SystemTime::now(), so make the test
deterministic by controlling the timestamp: modify the test to call a
deterministic variant of create_ipns_record (or add/use an overload that accepts
an explicit timestamp) and pass the same fixed timestamp for both sequence 0 and
100, then assert record0.data != record100.data and signatures differ; reference
create_ipns_record in the fix and add or use a parameter like timestamp or
create_ipns_record_with_time to ensure only the sequence changes between the two
records.

---

Nitpick comments:
In `@crates/core/src/ipns.rs`:
- Around line 18-20: Update the comment above the re-export of derive_ipns_name
to reference the correct current public path(s): mention that derive_ipns_name
is re-exported for backward compatibility so code/tests referencing the old path
(crate::crypto::ipns::derive_ipns_name) and the current path
(crate::ipns::derive_ipns_name) both continue to work; adjust the comment to
explicitly list these exact paths and keep the note about backward compatibility
next to the pub use of cipherbox_crypto::ipns_name::derive_ipns_name.

In `@crates/core/src/registry.rs`:
- Line 144: The assertion at the end of the test currently checks for either
"\"authStatus\"" or "\"status\"" but "authStatus" is impossible due to the
struct field being named `status` (and serde camelCase keeps it as "status"), so
remove the dead branch and change the assertion to only verify that the
serialized JSON contains "\"status\"" (update the assert! that currently
references both variants in the file containing the test/assertion).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 04e60c3f-8b14-46b7-ac86-9d847abe02a1

📥 Commits

Reviewing files that changed from the base of the PR and between 6ae9e78 and 617db07.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • apps/desktop/src-tauri/Cargo.toml
  • crates/core/src/bin.rs
  • crates/core/src/ipns.rs
  • crates/core/src/registry.rs
  • crates/crypto/src/ipns_name.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • .github/workflows/ci.yml
  • crates/crypto/src/ipns_name.rs
  • crates/core/src/bin.rs

Comment thread crates/core/src/ipns.rs
FSM1 and others added 5 commits March 24, 2026 21:09
Cargo can't resolve dep:fuser in feature definitions when fuser is
under [target.'cfg(unix)'.dependencies] — the feature reference fails
on Windows even when the feature isn't activated. Move fuser and
unicode-normalization back to [dependencies] (optional), keeping libc
under cfg(unix). The fuse feature gate + vendored build.rs early-return
prevent actual compilation on Windows.

Also fix TS build order: @cipherbox/crypto must build before
@cipherbox/core (core imports crypto types for DTS generation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 853876564786
The workspace [patch.crates-io] forces Cargo to resolve and compile the
vendored fuser on all platforms. Since fuser is Unix-only, split lib.rs
into a thin wrapper that gates all content behind #[cfg(unix)] via
include!("lib_impl.rs"). On Windows the crate compiles as empty.

Also restructure build.rs to use a separate unix_build() function
instead of early return (avoids unreachable code warnings).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 46cbfa9581df
The workspace [workspace.dependencies] entry for cipherbox-fuse didn't
specify default-features = false, so the desktop's { workspace = true,
default-features = false } was overridden by the workspace-level
definition. This caused the fuse feature (and thus fuser imports) to
be activated on Windows even with --no-default-features --features winfsp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6899c53b9c66
Summary of debugging progress, remaining issue (platform/windows/
super:: resolution), hypotheses, and debug commands for Windows session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 0d13d24c6aad
… test

Four root causes fixed:
- super:: path depth in nested mod implementation blocks (20 E0433 errors)
- pub(crate) visibility preventing cross-crate access (1 E0603 error)
- Missing cache re-export in desktop fuse bridge (1 E0432 error)
- ApiError not convertible to String via ? operator (6 E0277 errors)

Both CI cargo scripts now pass on Windows:
- cargo check --workspace --no-default-features --features winfsp
- cargo test --workspace --no-default-features --features winfsp

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FSM1 FSM1 requested a review from Copilot March 24, 2026 23:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 85 out of 134 changed files in this pull request and generated 3 comments.

Comment thread crates/api-client/src/ipns.rs Outdated
Comment thread crates/core/src/folder.rs Outdated
Comment thread .planning/ROADMAP.md
…messages

- Validate `success` field and non-empty CID in IPNS resolve response
- Update misleading "Encryption failed" error message to "Crypto operation failed"
- Mark all Phase 23 plan checkboxes as complete in ROADMAP.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6b372c5fae8c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@apps/desktop/src-tauri/src/fuse/mod.rs`:
- Around line 254-292: Add comprehensive tests for merge_folder_children in the
fuse crate by expanding the existing test module (the module that already
defines make_file, metadata, child_name and tests merge_both_empty,
merge_disjoint_children_union, merge_identical_uses_local). Specifically, add
tests that: verify local preference on conflicts with different ordering and
multiple conflicting entries (use merge_identical_uses_local pattern but with
several files), ensure union semantics preserve order and completeness for
disjoint sets (similar to merge_disjoint_children_union but with interleaved
inputs), stress-test with a large number of children to detect performance or
truncation issues, and cover orphaned-local and orphaned-remote cases where one
side has extra items; use the same FolderChild/FilePointer construction helpers
and assert on merged.children length and child_name() contents to locate
regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50968ea1-6908-480f-af60-069e39035bcf

📥 Commits

Reviewing files that changed from the base of the PR and between 617db07 and cd8c98b.

📒 Files selected for processing (15)
  • .github/workflows/ci.yml
  • .learnings/2026-03-25-windows-build-debug-on-platform.md
  • .planning/phases/23-rust-sdk-extraction/WINDOWS-BUILD-DEBUG.md
  • Cargo.toml
  • apps/desktop/src-tauri/Cargo.toml
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • apps/desktop/src-tauri/src/fuse/windows/mod.rs
  • apps/desktop/src-tauri/vendor/fuser/build.rs
  • apps/desktop/src-tauri/vendor/fuser/src/lib.rs
  • apps/desktop/src-tauri/vendor/fuser/src/lib_impl.rs
  • crates/fuse/Cargo.toml
  • crates/fuse/src/platform/windows/dir_ops.rs
  • crates/fuse/src/platform/windows/operations.rs
  • crates/fuse/src/platform/windows/read_ops.rs
  • crates/fuse/src/platform/windows/write_ops.rs
✅ Files skipped from review due to trivial changes (2)
  • .learnings/2026-03-25-windows-build-debug-on-platform.md
  • crates/fuse/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • Cargo.toml
  • apps/desktop/src-tauri/src/fuse/windows/mod.rs

Comment thread apps/desktop/src-tauri/src/fuse/mod.rs
Desktop app is now a thin Tauri shell after Rust SDK extraction —
most testable logic moved to workspace crates. Coverage drop is
expected and not actionable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: d429b7703e51
@FSM1 FSM1 merged commit 34bce7b into main Mar 24, 2026
25 checks passed
@FSM1 FSM1 deleted the feat/phase-23-rust-sdk-extraction branch March 25, 2026 22:42
FSM1 added a commit that referenced this pull request Jun 20, 2026
Address the actionable CodeRabbit findings on PR #529 that fall within
HARD-02 (crypto-signature & secret-leak hardening):

S2 — fail closed on PARTIAL signature fields. A resolve response carrying
some-but-not-all of {signatureV2, data, pubKey} previously fell into the
legacy allow path (signatureVerified=false), letting an attacker strip
fields to bypass D-02. Now only an all-absent record is treated as legacy;
any partial subset fails closed. Applied consistently across the three
verify paths: crates/api-client (Ok(None) only when all three absent, else
Ok(Some(false))), apps/web, and packages/sdk-core.

S1 — anchor the full signed IPNS value. The publish-time check matched only
the first CID substring via regex, so `/ipfs/<cid>/extra` could pass while
delegated routing published a divergent raw value. Compare the whole value
to `/ipfs/${metadataCid}` exactly.

S3 — close secret-leak gaps. crates/fuse resolve_folder_key_cached now
returns Zeroizing<Vec<u8>> (cache hits no longer hand back unzeroized plain
copies). Desktop mount pre-population wraps derived [u8;32] folder keys in
Zeroizing.

Test quality: replace the duplicate sdk-core S2 regression test with a
partial-fields fail-closed case; assert publish errors propagate
(rejects.toThrow) instead of swallowing in try/catch; add a vault rejected-
path zeroization guard; fix a bigint mock; make the API concurrent-batch
spec order-independent (Promise.allSettled preserves result order, not parse
call order). Added partial-field tests for web + sdk-core + api-client.

Deferred (captured as todos, out of HARD-02 scope or risky):
- per-file IPNS Conflict recorded as success (pre-existing #352, Phase 52)
- inode stable-ID identity reset on display-name fallback (Phase 52)
- validate embedded sequence when CAS omitted (risks non-CAS publish paths)
- bind verified record to resp.cid via CBOR decode (heavy lift)

Verified: SDK E2E 89/89, sdk-core 210, api 79 (ipns spec), web 61,
cargo api-client/crypto green, cargo check --workspace clean, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a91dbc72b885
FSM1 added a commit that referenced this pull request Jun 20, 2026
…zation (#529)

* docs(51): capture phase context

Entire-Checkpoint: 3af957aad5e8

* docs(state): record phase 51 context session

Entire-Checkpoint: 63ac3e2a0b38

* docs(51): drop Faro/logging todo 15 from scope

Entire-Checkpoint: 3972a20de96b

* docs(51): research phase crypto-signature-secret-leak-hardening

* docs(51): add research and validation strategy for crypto-signature hardening

* docs(51): create phase plan for crypto-signature & secret-leak hardening

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(51): add patterns map and finalize plan-check gate fixes

* test: add failing S1 embedded-vs-DTO CID and offset-aware sequence tests

- CID mismatch throws BadRequestException (400)
- Non-first-publish sequence mismatch throws BadRequestException (400)
- First-publish tolerates embedded seq 0n and 1n
- Valid CID+seq pass-through succeeds
- Anti-rollback ConflictException (409) regression guard

* feat: implement S1 embedded-vs-DTO CID and offset-aware sequence validation

- Add BadRequestException (400) when signedRecord embedded CID differs from
  metadataCid DTO (strict check via /ipfs/ regex extraction)
- Add offset-aware BadRequestException (400) when embedded sequence disagrees
  with expectedSequenceNumber: first-publish tolerates 0n/1n difference,
  subsequent publishes require exactly expectedSeq+1n
- Reuse already-parsed incoming record from anti-rollback block (parseIpnsRecord
  called at most once per request)
- CAS ConflictException (409) fires before S1 sequence check to preserve
  concurrent-modification semantics
- Fix pre-existing tests: align parseIpnsRecord mock embedded CID and sequence
  with DTO values now that S1 validates them (Rule 1 auto-fix)

* docs: complete S1 embedded-vs-DTO IPNS validation plan summary

- 79/79 tests pass; S1 CID + offset-aware sequence gates green
- 2 Rule-1 auto-fixes documented (CAS ordering, test mock alignment)

* test: add failing web ipns.service resolve tests RED

- Add 6 vitest cases covering fail-closed behavior for resolveIpnsRecord
- Tests 1-2 (present-but-invalid throw, key-substitution throw) FAIL as expected
- Tests 3-6 (absent-fields allow, valid pass, 404 null, non-404 propagate) pass
- Uses .test.ts suffix required by web vitest include pattern

* feat: make web resolveIpnsRecord fail-closed on invalid signatures

- Remove swallowing try/catch from inner verification block
- Throw on present-but-invalid signature (D-02, S2 fix)
- Throw on pubKey to ipnsName mismatch (D-02, key substitution guard)
- Keep allow+flag path for absent signature fields (D-03, legacy compat)
- Leave outer 404 catch unchanged so only 404 maps to null
- Mirrors sdk-core canonical behavior exactly

* docs: complete web ipns fail-closed resolve plan

* test: add failing verify_ipns_resolve_signature tests

- Add cipherbox-crypto workspace dep to crates/api-client/Cargo.toml
- Add signature_v2, data, pub_key Option<String> fields to IpnsResolveResponse
- Add stub verify_ipns_resolve_signature (unimplemented!) with 5 failing tests
- Tests: deserialize sig fields, absent→None, invalid→Some(false), valid→Some(true), name mismatch→Some(false)

* feat: implement verify_ipns_resolve_signature

- D-03: Ok(None) when any sig field absent (backward-compat allow+flag)
- D-02: Ok(Some(false)) on invalid Ed25519 signature (fail-closed)
- D-04: Ok(Some(true)) on valid sig + pubKey derives to matching IPNS name
- Ok(Some(false)) on pubKey length != 32 or name mismatch
- All 6 unit tests pass (GREEN)

* feat: Zeroizing unwrap_key and FUSE folder-key paths

S3/D-05: close raw Vec<u8> key escapes across Rust crates

- ecies::unwrap_key now returns Zeroizing<Vec<u8>> (zeroize on drop)
- resolve_folder_key BFS queue is VecDeque<(String, Zeroizing<Vec<u8>>)>
- get_folder_key returns Option<Zeroizing<Vec<u8>>>
- spawn_file_meta_reencrypt params source/dest_folder_key are Zeroizing
- UploadJournalResult.folder_key_for_file_meta is Option<Zeroizing<Vec<u8>>>
- Remove redundant Zeroizing::new() wrappers around unwrap_key call sites
- Update type annotations in write_ops and windows/write_ops
- Fix cross_language.rs and ecies unit tests for new return type

* feat: FUSE resolve_folder_key honors signature verification

S2/D-04: gate resolve_folder_key BFS descent on signature check

- Call verify_ipns_resolve_signature after each resolve_ipns in BFS
- Ok(None): warn and continue (D-03, absent fields = legacy record)
- Ok(Some(false)): return Err, fail closed (D-02, invalid signature)
- Ok(Some(true)): proceed (signature valid + name binding confirmed)
- Err: return Err surfacing the verification failure

cargo test -p cipherbox-api-client -p cipherbox-fuse: 66 passed, 0 failed

* docs: complete Rust S2 verify and S3 Zeroizing plan

- Add 51-03-SUMMARY.md with task outcomes, test results, deviations
- Update STATE.md plan counter (3→4), metrics, key decisions

* test: add S3 zeroization guard tests for ipns + vault and S2 regression guard

- Test A: assert ipnsPrivateKey buffer zeroed after successful createAndPublishIpnsRecord
- Test B: assert ipnsPrivateKey buffer zeroed even when publish throws (finally path)
- Test C: assert vaultKeyKeypair.privateKey zeroed after publishVaultKeyBlob
- Test D: S2 regression guard - resolveIpnsRecord throws on present-but-invalid signature
- Tests A/B/C fail RED (no fill(0) yet); Test D passes (S2 already correct)

S3/D-05 enforcement guard per T-47-01 caller-owns-key convention

* feat: add T-47-01 fill(0) to createAndPublishIpnsRecord and publishVaultKeyBlob

- Wrap createAndPublishIpnsRecord body in try/finally; finally zeroes params.ipnsPrivateKey
- Wrap publishVaultKeyBlob publish logic in try/finally; finally zeroes vaultKeyKeypair.privateKey
- Both functions are the terminal owners of their respective key buffers (D-05/T-47-01)
- S3 zeroization guard tests A/B/C now pass GREEN; S2 regression test D still passes

* feat: document and guard updateFolderMetadataAndPublish zeroization decision

- Add T-47-01/D-05 skip comment to folder/index.ts: caller (client.ts folderTree)
  retains ownership of ipnsPrivateKey and folderKey across the session lifetime
- Add SKIP guard test to folder.test.ts: asserts keys are UNCHANGED after return,
  documenting deliberate non-zeroing and preventing accidental future fill(0)
- Contrast with updateFileMetadata which DOES zero (per-use key, terminal consumer)
- Full sdk-core suite: 209 tests passing

* docs: complete 51-04 sdk-core key zeroization plan

- 51-04-SUMMARY.md: zeroization convention applied to ipns + vault paths
- STATE.md: advance to plan 4/4 complete, add 51-04 decision
- Full sdk-core suite: 209 tests passing

* style: apply cargo fmt to api-client ipns and types

Re-stage cargo fmt reformatting of the in-scope 51-03 files that a later
pre-commit hook left as uncommitted drift in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: verification report - all 4 deliverables PASS

S1/S2/S3 hardening for HARD-02 verified: API embedded-vs-DTO 400,
web + Rust fail-closed IPNS verification, sdk-core + Rust key zeroization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(desktop): convert Zeroizing unwrap_key result to Vec in load_vault_key

Entire-Checkpoint: 0ddf318e81ed

* docs(51): add security threat verification

Entire-Checkpoint: ecf81ac2076b

* docs(51): update validation strategy post-execution audit

Entire-Checkpoint: e0f0794d988f

* refactor(api-client): dedupe IPNS base64 decode into a local closure

Entire-Checkpoint: 530961db1d56

* fix: wrap residual key material in Zeroizing for memory hygiene

Completes Phase 51 D-05 exhaustive zeroization with three defense-in-depth
memory-hygiene fixes flagged by CodeRabbit:

- SDK state root_folder_key field is now Option<Zeroizing<Vec<u8>>> instead of
  plain Vec, so the decrypted root key is wiped on drop. Desktop vault.rs stores
  the unwrap_key Zeroizing directly; mount_filesystem (macOS + winfsp) takes
  Zeroizing and no longer re-wraps.
- replay_for_vault folder-key cache is HashMap<String, Zeroizing<Vec<u8>>>, so
  cached folder keys are zeroized when the cache drops at end of replay.
- spawn_file_meta_reencrypt wraps its fixed-size [u8; 32] source/dest key copies
  in Zeroizing.

root_ipns_private_key is out of scope (not flagged, HKDF-derived).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: beeb90918390

* fix(fuse): zeroize transient fixed-size key arrays in FUSE operations

Wrap transient [u8; 32] key arrays built via try_into() from decrypted
key material (file keys, folder keys, IPNS keys) in zeroize::Zeroizing so
plaintext key bytes are wiped on scope exit instead of lingering on the
stack. Mirrors the established spawn_file_meta_reencrypt pattern. Applies
to both the macOS/Linux FUSE and Windows WinFsp operation paths, keeping
the two platforms in lockstep. Completes the Phase 51 D-05 exhaustive
zeroization sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: b05ee7305656

* docs(todos): capture deferred CodeRabbit findings from phase 51 review

Entire-Checkpoint: 2d8dae1d5c16

* docs(51): ship phase 51 — PR #529

Entire-Checkpoint: cd32c0e862b7

* chore(release): set release targets for PR #529

* fix: stop zeroing reused IPNS key buffer in createAndPublishIpnsRecord

Phase 51 (S3/D-05) added `params.ipnsPrivateKey.fill(0)` in a finally to
createAndPublishIpnsRecord, treating it as a "buffer-owning boundary". It is
not: it is a callee that receives a caller-owned, reused buffer. The SDK
client caches per-folder IPNS keys in its folderTree (zeroed only on
destroy()) and passes the same buffer on every mutation; publishWithCas
reuses the buffer across CAS retries. After the first publish the buffer was
all-zero, so the next publish derived the wrong public key and the API
rejected it with 400 "publicKey does not correspond to the given ipnsName",
cascading "Child not found" failures across every publish-dependent flow.

This broke 48/89 SDK E2E tests (the only gate exercising the real
cross-package publish round-trip; the phase's unit checks and the S1 spec
mocked parseIpnsRecord, so it slipped through).

Fix: createAndPublishIpnsRecord no longer zeros its ipnsPrivateKey param,
matching publishWithCas's contract ("NEVER zeroes key material — callers are
responsible") and updateFolderMetadataAndPublish ("CALLER RETAINS
OWNERSHIP"). No key leak — terminal owners still zero: client.destroy()
(cached folderTree keys), clearBytes()/finally (transient unwrapped keys),
and publishVaultKeyBlob / shared-write.ts (freshly-derived keypairs). The
S3/D-05 guard tests A/B were inverted to assert the caller-owned buffer is
preserved.

Verified locally against the full stack: SDK E2E 89/89, sdk-core 209/209,
api 893/893, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: bfbe30d8f64a

* fix: harden IPNS signature verification and key zeroization per review

Address the actionable CodeRabbit findings on PR #529 that fall within
HARD-02 (crypto-signature & secret-leak hardening):

S2 — fail closed on PARTIAL signature fields. A resolve response carrying
some-but-not-all of {signatureV2, data, pubKey} previously fell into the
legacy allow path (signatureVerified=false), letting an attacker strip
fields to bypass D-02. Now only an all-absent record is treated as legacy;
any partial subset fails closed. Applied consistently across the three
verify paths: crates/api-client (Ok(None) only when all three absent, else
Ok(Some(false))), apps/web, and packages/sdk-core.

S1 — anchor the full signed IPNS value. The publish-time check matched only
the first CID substring via regex, so `/ipfs/<cid>/extra` could pass while
delegated routing published a divergent raw value. Compare the whole value
to `/ipfs/${metadataCid}` exactly.

S3 — close secret-leak gaps. crates/fuse resolve_folder_key_cached now
returns Zeroizing<Vec<u8>> (cache hits no longer hand back unzeroized plain
copies). Desktop mount pre-population wraps derived [u8;32] folder keys in
Zeroizing.

Test quality: replace the duplicate sdk-core S2 regression test with a
partial-fields fail-closed case; assert publish errors propagate
(rejects.toThrow) instead of swallowing in try/catch; add a vault rejected-
path zeroization guard; fix a bigint mock; make the API concurrent-batch
spec order-independent (Promise.allSettled preserves result order, not parse
call order). Added partial-field tests for web + sdk-core + api-client.

Deferred (captured as todos, out of HARD-02 scope or risky):
- per-file IPNS Conflict recorded as success (pre-existing #352, Phase 52)
- inode stable-ID identity reset on display-name fallback (Phase 52)
- validate embedded sequence when CAS omitted (risks non-CAS publish paths)
- bind verified record to resp.cid via CBOR decode (heavy lift)

Verified: SDK E2E 89/89, sdk-core 210, api 79 (ipns spec), web 61,
cargo api-client/crypto green, cargo check --workspace clean, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a91dbc72b885

* fix: close empty-string signature downgrade and axios 404 gaps

Second round of CodeRabbit follow-ups on PR #529 (all adversarially verified):

S2 — empty-string fail-closed. The resolve signature gate used truthy checks
(`if (signatureV2 || data || pubKey)`), so a record with all three fields
present as empty strings ('') was downgraded to the legacy allow path. Switch
to nullish presence checks (`!= null`) in both apps/web and packages/sdk-core
so present-but-empty fields are treated as present and fail closed; only
all-omitted records take the legacy path. (Rust verifier already fails closed
on empty via base64-decode + verify.)

Web 404 detection. apps/web resolve only checked `error.status`, but real
axios errors from the api-client surface status at `error.response?.status`,
so live 404s would throw instead of returning null. Now checks
`error.status ?? error.response?.status`, matching sdk-core.

S3 — no transient plaintext key. Added a `zeroizing_32_from_slice` helper
(preallocate `Zeroizing<[u8;32]>`, length-check, then `copy_from_slice`) and
applied it to the file/folder/IPNS key sites in crates/fuse operations.rs and
platform/windows/operations.rs, so no plain `[u8;32]` temporary is
materialized before the zeroizing wrapper.

Tests: add empty-string fail-closed cases (web + sdk-core); web 404 test now
mocks the axios `error.response` shape plus a fallback `error.status` case;
vault test D now exercises the real publish-failure path (upload succeeds,
publish rejects) instead of the upload path.

Verified: SDK E2E 89/89, sdk-core 211, web 63, cargo fuse 60, lint + cargo
check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: b3b9ba579e3d

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

2 participants