From 2fe73dc2e9cd46f248b434728419168189d781c4 Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Wed, 24 Jun 2026 19:21:13 -0400 Subject: [PATCH] feat(fula-js): expose putFlat, getFlat, listFilesFromForest (forest-tracked I/O) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WASM bindings only exposed `putEncryptedWithType` — a RAW encrypted-object PUT that writes the blob but never registers it in the bucket's private-forest index. Objects written that way are readable only by their exact obfuscated key and are INVISIBLE to listDecrypted / listFilesFromForest / listDirectory. That is why the hosted MCP Worker's AI-workspace writes never appeared in FxFiles. Add three additive bindings wrapping EXISTING, unchanged fula-client methods that FxFiles' own Flutter FFI already uses for every file it writes/reads: - putFlat -> EncryptedClient::put_object_flat (upsert + flush_forest) - getFlat -> EncryptedClient::get_object_flat (forest + CID-aware read) - listFilesFromForest -> EncryptedClient::list_files_from_forest (forest enumeration) No fula-client logic changes; purely additive exports. Verified end-to-end against the prod gateway via a Node harness on the freshly-built wasm: putFlat -> listFilesFromForest enumerates the write -> getFlat round-trips byte-exact. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0161UGEJmTpM6DM2cVZyV6Ev --- crates/fula-js/src/lib.rs | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/fula-js/src/lib.rs b/crates/fula-js/src/lib.rs index 74a154e..a7f91fc 100644 --- a/crates/fula-js/src/lib.rs +++ b/crates/fula-js/src/lib.rs @@ -815,6 +815,35 @@ pub async fn put_encrypted_with_type( .map_err(|e| JsError::new(&format!("Serialization error: {}", e))) } +/// Upload encrypted data AND register it in the bucket's private-forest index, +/// then flush the forest manifest. Unlike `putEncryptedWithType` (a raw +/// encrypted-object PUT that never touches the forest), this calls the +/// forest-tracked `put_object_flat` — REQUIRED for the object to appear in +/// `listDecrypted` / `listFilesFromForest` / `listDirectory`. The hosted MCP +/// MUST use this for AI-workspace writes so the owner can enumerate them. It is +/// the same write FxFiles' own `uploadObject` already uses for every file. +#[wasm_bindgen(js_name = putFlat)] +pub async fn put_flat( + client: &EncryptedClient, + bucket: &str, + key: &str, + data: &[u8], + content_type: &str, +) -> Result { + let guard = client.inner.lock().await; + let result = guard + .put_object_flat(bucket, key, Bytes::from(data.to_vec()), Some(content_type)) + .await + .map_err(|e| JsError::new(&format!("Upload failed: {}", e)))?; + + let js_result = JsPutResult { + etag: result.etag, + version_id: result.version_id, + }; + serde_wasm_bindgen::to_value(&js_result) + .map_err(|e| JsError::new(&format!("Serialization error: {}", e))) +} + /// Download and decrypt data by original key /// /// @param client - EncryptedClient handle @@ -858,6 +887,25 @@ pub async fn get_decrypted_by_storage_key( Ok(data.to_vec()) } +/// Download + decrypt a file by its logical path via the forest — the read that +/// PAIRS with `putFlat`. Resolves the key through the private-forest index and +/// recovers by CID when the obfuscated storage key is gc-orphaned, unlike +/// `getDecrypted` (which reads the raw obfuscated key directly and fails on a +/// forest-tracked write). Mirrors FxFiles' `getFlat` (the Flutter FFI's +/// `get_flat`), so the hosted MCP can read back exactly what it `putFlat`-wrote. +#[wasm_bindgen(js_name = getFlat)] +pub async fn get_flat( + client: &EncryptedClient, + bucket: &str, + key: &str, +) -> Result, JsError> { + let guard = client.inner.lock().await; + let data = guard.get_object_flat(bucket, key) + .await + .map_err(|e| client_error_to_js_error("get_flat", e))?; + Ok(data.to_vec()) +} + /// Delete an encrypted object #[wasm_bindgen(js_name = deleteEncrypted)] pub async fn delete_encrypted( @@ -918,6 +966,37 @@ pub async fn list_decrypted( .map_err(|e| JsError::new(&format!("Serialization error: {}", e))) } +/// List the bucket's files straight from the decrypted private-forest index +/// (the "catalog") — the correct enumeration for flatNamespace + metadata-privacy +/// buckets. Unlike `listDecrypted` (which filters a prefix against the OBFUSCATED +/// storage keys and therefore returns 0 for such buckets), this walks the forest +/// and returns decrypted original keys. Mirrors FxFiles' `listFromForest` (the +/// Flutter FFI's `list_from_forest`), so the hosted MCP enumerates the same way +/// the app does. +#[wasm_bindgen(js_name = listFilesFromForest)] +pub async fn list_files_from_forest( + client: &EncryptedClient, + bucket: &str, +) -> Result { + let guard = client.inner.lock().await; + let result = guard.list_files_from_forest(bucket) + .await + .map_err(|e| JsError::new(&format!("List failed: {}", e)))?; + + let js_result: Vec = result.into_iter().map(|m| JsFileMetadata { + storage_key: m.storage_key, + original_key: m.original_key, + size: m.original_size, + content_type: m.content_type, + created_at: m.created_at, + modified_at: m.modified_at, + is_encrypted: m.is_encrypted, + }).collect(); + + serde_wasm_bindgen::to_value(&js_result) + .map_err(|e| JsError::new(&format!("Serialization error: {}", e))) +} + /// List directory structure #[wasm_bindgen(js_name = listDirectory)] pub async fn list_directory(