diff --git a/.gitignore b/.gitignore index c51b84d..a882cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .env .devenv CLAUDE.md +AGENTS.md grant_proposal.md **/**.db diff --git a/crates/weaver-app/.env-example b/crates/weaver-app/.env-example new file mode 100644 index 0000000..5d82bc4 --- /dev/null +++ b/crates/weaver-app/.env-example @@ -0,0 +1,10 @@ +WEAVER_APP_ENV="dev" +WEAVER_APP_HOST="http://localhost" +WEAVER_APP_DOMAIN="" +WEAVER_PORT=8080 +WEAVER_APP_SCOPES="atproto transition:generic" +WEAVER_CLIENT_NAME="Weaver" + +WEAVER_LOGO_URI="" +WEAVER_TOS_URI="" +WEAVER_PRIVACY_POLICY_URI="" diff --git a/crates/weaver-app/.env-prod b/crates/weaver-app/.env-prod deleted file mode 100644 index 76ce3db..0000000 --- a/crates/weaver-app/.env-prod +++ /dev/null @@ -1,10 +0,0 @@ -WEAVER_APP_ENV="prod" -WEAVER_APP_HOST="https://alpha.weaver.sh" -WEAVER_APP_DOMAIN="https://alpha.weaver.sh" -WEAVER_PORT=8080 -WEAVER_APP_SCOPES="atproto transition:generic" -WEAVER_CLIENT_NAME="Weaver" - -WEAVER_LOGO_URI="https://alpha.weaver.sh/favicon.ico" -WEAVER_TOS_URI="" -WEAVER_PRIVACY_POLICY_URI="" diff --git a/crates/weaver-app/src/components/editor/component.rs b/crates/weaver-app/src/components/editor/component.rs index 7734fb7..37f665d 100644 --- a/crates/weaver-app/src/components/editor/component.rs +++ b/crates/weaver-app/src/components/editor/component.rs @@ -3,6 +3,7 @@ use dioxus::prelude::*; use jacquard::IntoStatic; use jacquard::cowstr::ToCowStr; +use jacquard::identity::resolver::IdentityResolver; use jacquard::types::blob::BlobRef; use jacquard::types::ident::AtIdentifier; use weaver_api::sh_weaver::embed::images::Image; @@ -88,6 +89,30 @@ pub fn MarkdownEditor(initial_content: Option, entry_uri: Option Ok(None) => { // No existing state - check if we need to load entry content if let Some(ref uri) = entry_uri { + // Check that this entry belongs to the current user + if let Some(current_did) = fetcher.current_did().await { + let entry_authority = uri.authority(); + let is_own_entry = match entry_authority { + AtIdentifier::Did(did) => did == ¤t_did, + AtIdentifier::Handle(handle) => { + // Resolve handle to DID and compare + match fetcher.client.resolve_handle(handle).await { + Ok(resolved_did) => resolved_did == current_did, + Err(_) => false, + } + } + }; + if !is_own_entry { + tracing::warn!( + "Cannot edit entry belonging to another user: {}", + entry_authority + ); + return LoadResult::Failed( + "You can only edit your own entries".to_string() + ); + } + } + // Try to load the entry content from PDS match load_entry_for_editing(&fetcher, uri).await { Ok(loaded) => { @@ -114,7 +139,7 @@ pub fn MarkdownEditor(initial_content: Option, entry_uri: Option entry_ref: Some(loaded.entry_ref), edit_root: None, last_diff: None, - is_synced: false, + synced_version: None, // Fresh from entry, never synced }); } Err(e) => { @@ -137,7 +162,7 @@ pub fn MarkdownEditor(initial_content: Option, entry_uri: Option entry_ref: None, edit_root: None, last_diff: None, - is_synced: false, + synced_version: None, // New doc, never synced }) } Err(e) => { diff --git a/crates/weaver-app/src/components/editor/document.rs b/crates/weaver-app/src/components/editor/document.rs index a99ddb6..2c55a12 100644 --- a/crates/weaver-app/src/components/editor/document.rs +++ b/crates/weaver-app/src/components/editor/document.rs @@ -212,9 +212,10 @@ pub struct LoadedDocState { pub edit_root: Option>, /// StrongRef to the most recent sh.weaver.edit.diff record. pub last_diff: Option>, - /// Whether the current doc state is synced with PDS. - /// False if local has changes not yet pushed to PDS. - pub is_synced: bool, + /// Version vector of the last known PDS state. + /// Used to determine what changes need to be synced. + /// None if never synced to PDS. + pub synced_version: Option, } impl PartialEq for LoadedDocState { @@ -993,12 +994,8 @@ impl EditorDocument { }; let loro_cursor = content.get_cursor(cursor_offset, Side::default()); - // Track sync state - if synced, record current version - let last_synced_version = if state.is_synced { - Some(doc.oplog_vv()) - } else { - None - }; + // Use the synced version from state (tracks the PDS version vector) + let last_synced_version = state.synced_version; Self { doc, diff --git a/crates/weaver-app/src/components/editor/sync.rs b/crates/weaver-app/src/components/editor/sync.rs index c3ed3fe..5da037b 100644 --- a/crates/weaver-app/src/components/editor/sync.rs +++ b/crates/weaver-app/src/components/editor/sync.rs @@ -671,7 +671,7 @@ pub async fn load_and_merge_document( entry_ref: local.entry_ref, // Restored from localStorage edit_root: None, last_diff: None, - is_synced: false, // Local-only, not synced to PDS + synced_version: None, // Local-only, never synced to PDS })) } @@ -692,12 +692,15 @@ pub async fn load_and_merge_document( } } + // Capture the version after loading all PDS state - this is our sync baseline + let synced_version = Some(doc.oplog_vv()); + Ok(Some(LoadedDocState { doc, entry_ref: None, // Entry ref comes from the entry itself, not edit state edit_root: Some(pds.root_ref), last_diff: pds.last_diff_ref, - is_synced: true, // Just loaded from PDS, fully synced + synced_version, // Just loaded from PDS, fully synced })) } @@ -705,6 +708,19 @@ pub async fn load_and_merge_document( // Both exist - merge using CRDT tracing::debug!("Merging document from localStorage and PDS"); + // First, reconstruct the PDS state to get its version vector + let pds_doc = LoroDoc::new(); + if let Err(e) = pds_doc.import(&pds.root_snapshot) { + tracing::warn!("Failed to import PDS root snapshot for VV: {:?}", e); + } + for updates in &pds.diff_updates { + if let Err(e) = pds_doc.import(updates) { + tracing::warn!("Failed to apply PDS diff for VV: {:?}", e); + } + } + let pds_version = pds_doc.oplog_vv(); + + // Now create the merged doc let doc = LoroDoc::new(); // Import local snapshot first @@ -724,12 +740,14 @@ pub async fn load_and_merge_document( } } + // Use the PDS version as our sync baseline - any local changes + // beyond this will be detected as unsynced Ok(Some(LoadedDocState { doc, entry_ref: local.entry_ref, // Restored from localStorage edit_root: Some(pds.root_ref), last_diff: pds.last_diff_ref, - is_synced: false, // Local had state, may have unsynced changes + synced_version: Some(pds_version), })) } } diff --git a/crates/weaver-app/src/env.rs b/crates/weaver-app/src/env.rs index b8c902b..f8a1ad7 100644 --- a/crates/weaver-app/src/env.rs +++ b/crates/weaver-app/src/env.rs @@ -1,11 +1,11 @@ // This file is automatically generated by build.rs #[allow(unused)] -pub const WEAVER_APP_ENV: &'static str = "dev"; +pub const WEAVER_APP_ENV: &'static str = "prod"; #[allow(unused)] -pub const WEAVER_APP_HOST: &'static str = "http://localhost"; +pub const WEAVER_APP_HOST: &'static str = "https://alpha.weaver.sh"; #[allow(unused)] -pub const WEAVER_APP_DOMAIN: &'static str = ""; +pub const WEAVER_APP_DOMAIN: &'static str = "https://alpha.weaver.sh"; #[allow(unused)] pub const WEAVER_PORT: &'static str = "8080"; #[allow(unused)] @@ -13,7 +13,7 @@ pub const WEAVER_APP_SCOPES: &'static str = "atproto transition:generic"; #[allow(unused)] pub const WEAVER_CLIENT_NAME: &'static str = "Weaver"; #[allow(unused)] -pub const WEAVER_LOGO_URI: &'static str = ""; +pub const WEAVER_LOGO_URI: &'static str = "https://alpha.weaver.sh/favicon.ico"; #[allow(unused)] pub const WEAVER_TOS_URI: &'static str = ""; #[allow(unused)] diff --git a/crates/weaver-cli/src/main.rs b/crates/weaver-cli/src/main.rs index 86a202a..cd94c7d 100644 --- a/crates/weaver-cli/src/main.rs +++ b/crates/weaver-cli/src/main.rs @@ -388,14 +388,14 @@ async fn publish_notebook(source: PathBuf, title: String, store_path: PathBuf) - // Use WeaverExt to upsert entry (handles notebook + entry creation/updates) use jacquard::http_client::HttpClient; use weaver_common::WeaverExt; - let (entry_uri, was_created) = agent + let (entry_ref, was_created) = agent .upsert_entry(&title, entry_title.as_ref(), entry) .await?; if was_created { - println!(" ✓ Created new entry: {}", entry_uri.as_ref()); + println!(" ✓ Created new entry: {}", entry_ref.uri.as_ref()); } else { - println!(" ✓ Updated existing entry: {}", entry_uri.as_ref()); + println!(" ✓ Updated existing entry: {}", entry_ref.uri.as_ref()); } }