From 673f9e2261f1da4ef120915283c784122f43b3fc Mon Sep 17 00:00:00 2001 From: Scott Reimers Date: Thu, 14 May 2026 14:01:57 -0400 Subject: [PATCH] feat(fof-layer2): wire FoF gating into post-create path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads optional fof_gating through create_post_inner so a published post can carry the author-signed gating snapshot at publish time (covered by PostId = BLAKE3(Post)). New public entry point: Node::create_post_with_fof_comments(content, attachments) -> (PostId, Post, PostVisibility, cek: [u8; 32]) Builds the FoFCommentGating block via fof::build_fof_comment_gating from the default persona's keyring (own V_me + every received V_x), then calls create_post_inner with VisibilityIntent::Public (Mode 2 keeps body public). Returns the per-post CEK to the caller for local caching (decrypting one's own comments later). Existing create_post / create_post_as / create_post_with_visibility threads `None` through the new arg — back-compat for non-FoF posts. CommentPolicy::FriendsOfFriends is NOT published as a SetPolicy diff in this commit; the post's `fof_gating` field is itself the signal that the post supports FoF commenting. The four-check CDN verify gate (next commit) reads fof_gating directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/core/src/node.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 44fb070..02940d6 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -1014,6 +1014,7 @@ impl Node { content, intent, attachment_data, + None, ).await } @@ -1038,9 +1039,43 @@ impl Node { content, intent, attachment_data, + None, ).await } + /// FoF Layer 2: create a Mode 2 post (public body, FoF-gated + /// comments). Intent is Public; the FoF gating block is built + /// from the default persona's keyring and embedded in + /// `Post.fof_gating`. The author retains the per-post CEK locally + /// for decrypting their own comments later. + /// + /// Returns `(post_id, post, visibility, cek)`. `visibility` is + /// always Public for Mode 2. + pub async fn create_post_with_fof_comments( + &self, + content: String, + attachment_data: Vec<(Vec, String)>, + ) -> anyhow::Result<(PostId, Post, PostVisibility, [u8; 32])> { + // Build the gating block from the default persona's keyring. + let built = { + let storage = self.storage.get().await; + crate::fof::build_fof_comment_gating(&*storage, &self.default_posting_id)? + .ok_or_else(|| anyhow::anyhow!( + "default persona has no V_me; rotate or recreate before FoF posts" + ))? + }; + let cek = built.cek; + let (post_id, post, visibility) = self.create_post_inner( + &self.default_posting_id, + &self.default_posting_secret, + content, + VisibilityIntent::Public, + attachment_data, + Some(built.gating), + ).await?; + Ok((post_id, post, visibility, cek)) + } + async fn create_post_inner( &self, posting_id: &NodeId, @@ -1048,6 +1083,7 @@ impl Node { content: String, intent: VisibilityIntent, attachment_data: Vec<(Vec, String)>, + fof_gating: Option, ) -> anyhow::Result<(PostId, Post, PostVisibility)> { // Validate attachments if attachment_data.len() > 4 { @@ -1163,7 +1199,7 @@ impl Node { content: final_content, attachments, timestamp_ms: now, - fof_gating: None, + fof_gating, }; let post_id = compute_post_id(&post);