feat: plugins, sync UI, daemon sidecar, tests, release pipeline#2
Conversation
- Plugin Manager: WASM plugin system (wasmtime), auto-load from plugins dir, load/unload UI with file picker, list loaded plugins - Sync: HTTP push/pull sync client and relay server (clipboard-api now has a runnable binary), sync config UI (server URL, token, enable toggle, Sync Now with status feedback) - Daemon sidecar: desktop app probes socket on launch, spawns daemon automatically if not running, kills it on quit - Tests: 26 tests across clipboard-db (13) and clipboard-encryption (12) - ClipboardManager: implemented in clipboard-core using arboard (no longer stub) - Unused imports cleaned up across clipboard-plugin and clipboard-api - CI: modernized to dtolnay/rust-toolchain + Swatinem/rust-cache - Release workflow: builds macOS arm64+x64 .dmg, Windows .exe/.msi, Linux .deb/.AppImage on version tags via tauri-action - README: full rewrite with install instructions, CLI reference, plugin contract, sync setup, developer guide, screenshot - CHANGELOG.md added
📝 WalkthroughWalkthroughThe change expands OpenPaste with encrypted clipboard persistence, tags, plugins, HTTP synchronization, desktop controls, CLI commands, image handling, API endpoints, and automated CI/release workflows. ChangesOpenPaste feature integration
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant DesktopApp
participant ClipboardDaemon
participant Database
participant SyncRelay
User->>DesktopApp: configure vault, tags, plugins, or sync
DesktopApp->>ClipboardDaemon: invoke IPC command
ClipboardDaemon->>Database: read or update application state
ClipboardDaemon->>SyncRelay: push or pull synchronized items
ClipboardDaemon-->>DesktopApp: return state or operation result
DesktopApp-->>User: render clipboard history and settings
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (12)
crates/clipboard-api/src/api.rs-95-98 (1)
95-98: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
totalfield returns page count, not total database count.
"total": out.len()returns the number of items in the current page, not the total count of all items in the database. Conventionally,totalin a paginated response means the total item count for pagination. A client requestinglimit=50&offset=0with 1000 items in the DB would seetotal: 50and stop paginating.Rename to
countorreturnedto avoid confusion, or add a COUNT query for the true total.🩹 Proposed fix: rename to `count`
- Json(json!({ "items": out, "total": out.len() })) + Json(json!({ "items": out, "count": out.len() }))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-api/src/api.rs` around lines 95 - 98, In the item-list response built by the list endpoint, replace the misleading "total" JSON field derived from out.len() with "count" (or "returned") to clearly represent the number of items in the current page; update any related response handling or tests accordingly.crates/clipboard-sync/src/sync.rs-98-131 (1)
98-131: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPush silently caps at 1000 items and filters in memory.
list_items(1000, 0)fetches only the 1000 most recent rows before thesincefilter is applied in memory. On an initial full sync (since = None), any device with more than 1000 items will never push the older ones. Consider pushing in paged batches, or pushing directly from acreated_at > sincequery so the DB does the filtering and bounding.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-sync/src/sync.rs` around lines 98 - 131, Update SyncService::push to avoid the fixed list_items(1000, 0) limit: use a database query that applies the since_rfc3339 created_at filter and paginate through all matching records, or repeatedly fetch batches with an advancing offset/cursor. Ensure every matching item is transformed into SyncItem and pushed, including older records during a full sync, while preserving the existing return count and empty-result behavior.crates/clipboard-db/src/database.rs-199-246 (1)
199-246: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRetention/limit maintenance can delete pinned or favorite items.
Both
cleanup_old_items(age-based) andenforce_max_items(count-based, oldestcreated_atfirst) delete rows without excludingpinned/favoriteitems. Users typically expect pinned items to survive automatic pruning; silently deleting them is data loss.🛡️ Suggested guard (exclude pinned/favorite)
- let result = sqlx::query( - "DELETE FROM clipboard_items WHERE created_at < ?" - ) + let result = sqlx::query( + "DELETE FROM clipboard_items WHERE created_at < ? AND pinned = 0 AND favorite = 0" + )Apply an equivalent
WHERE pinned = 0 AND favorite = 0filter to theenforce_max_itemssubquery as well.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-db/src/database.rs` around lines 199 - 246, Update cleanup_old_items and enforce_max_items so automatic deletion excludes protected rows by adding pinned = 0 AND favorite = 0 to their DELETE conditions, including the enforce_max_items ID-selection subquery; preserve existing retention and count behavior for unprotected items.crates/clipboard-core/src/clipboard.rs-57-69 (1)
57-69: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winAvoid silent data corruption with
from_utf8_lossy.
String::from_utf8_lossysilently replaces invalid UTF-8 sequences with U+FFFD, which corrupts non-UTF-8 content written to the clipboard without any error signal. Usestd::str::from_utf8and return an error on failure so callers can handle the case explicitly.🛡️ Proposed fix
pub async fn set(&self, item: &ClipboardItem) -> Result<(), ClipboardError> { match item.content_type { ContentType::Image => return Err(ClipboardError::UnsupportedContentType), _ => {} } - let text = String::from_utf8_lossy(&item.content).to_string(); + let text = std::str::from_utf8(&item.content) + .map_err(|e| ClipboardError::AccessFailed(format!("Invalid UTF-8 content: {}", e)))?; let mut ctx = arboard::Clipboard::new() .map_err(|e| ClipboardError::AccessFailed(e.to_string()))?; ctx.set_text(&text) .map_err(|e| ClipboardError::AccessFailed(e.to_string())) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-core/src/clipboard.rs` around lines 57 - 69, ClipboardCore::set currently replaces invalid UTF-8 via String::from_utf8_lossy, silently corrupting clipboard content. Replace it with std::str::from_utf8 and map validation failures to the appropriate ClipboardError, then pass the validated string or str reference to ctx.set_text while preserving existing access-error handling.crates/clipboard-daemon/src/main.rs-561-577 (1)
561-577: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
SetSyncConfigcannot clear an API token and swallows its save error.When
api_tokenisNonethe storedsync_api_tokenis left intact, so a user can never remove a previously saved token. Additionally the token upsert useslet _ =, so its failure is excluded from theresultssuccess check and reported as success.🔧 Suggested handling
- let results = vec![ - database_clone.upsert_setting("sync_server_url", &server_url).await, - database_clone.upsert_setting("sync_enabled", &enabled.to_string()).await, - ]; - if let Some(token) = &api_token { - let _ = database_clone.upsert_setting("sync_api_token", token).await; - } + let mut results = vec![ + database_clone.upsert_setting("sync_server_url", &server_url).await, + database_clone.upsert_setting("sync_enabled", &enabled.to_string()).await, + ]; + // Persist an empty string to clear the token when None + results.push( + database_clone + .upsert_setting("sync_api_token", api_token.as_deref().unwrap_or("")) + .await, + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-daemon/src/main.rs` around lines 561 - 577, Update the SetSyncConfig handler to always persist the API token setting, using an appropriate empty or cleared value when api_token is None, so existing tokens can be removed. Include the token upsert result in the results collection rather than discarding it with let _, ensuring any failure returns IpcMessage::Error.crates/clipboard-daemon/src/main.rs-679-707 (1)
679-707: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winLoaded plugins are never applied to captured clipboard content
crates/clipboard-daemon/src/main.rs:679-707
PluginManager::process()is never called on the capture path, so auto-loaded WASM plugins currently have no effect on what gets persisted. Call it before encrypting/storing if this pipeline is intended.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-daemon/src/main.rs` around lines 679 - 707, Loaded plugins are not applied before captured content is persisted. In the capture/save pipeline containing the encryption block and db_item construction, invoke PluginManager::process() on item.content before encryption, use the processed content for encryption and storage, and handle any processing errors according to the existing error-handling conventions..github/workflows/ci.yml-22-23 (1)
22-23: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winSet
persist-credentials: falseon checkout steps.
actions/checkout@v4persists theGITHUB_TOKENin.git/configby default. Neither CI job pushes back to the repository, so disabling credential persistence reduces the attack surface if a later step is compromised.🔒 Proposed fix (apply to both checkout steps)
- name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: falseAlso applies to: 66-67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 22 - 23, Update both checkout steps using actions/checkout@v4 in the CI workflow to set persist-credentials: false, preventing the GITHUB_TOKEN from being stored in the repository’s Git configuration.Source: Linters/SAST tools
apps/cli/src/main.rs-188-194 (1)
188-194: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNormalize ID comparison across all CLI commands.
GetandCopycompare IDs as strings (i.id == id), whilePin,Favorite, andDeleteparse toi64first. A user passingopenpaste get 01would fail to find item1, butopenpaste pin 01would succeed. Parse toi64consistently in all commands.🔧 Proposed fix for `Get` and `Copy` ID matching
Commands::Get { id } => { + let _id_i64: i64 = id.parse().map_err(|_| anyhow!("Invalid ID: {}", id))?; // Fetch full list and find by ID (no single-item IPC yet) let c = client().await; match c.send(IpcMessage::GetHistory).await { Ok(IpcMessage::ClipboardHistory { items }) => { - let item = items.iter().find(|i| i.id == id) + let item = items.iter().find(|i| i.id.parse::<i64>().ok() == Some(_id_i64)) .ok_or_else(|| anyhow!("Item {} not found", id))?;Apply the same pattern to
Copy(line 216).Also applies to: 211-218, 236-237
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cli/src/main.rs` around lines 188 - 194, Normalize ID handling across the CLI command handlers: in Get and Copy, parse the user-provided id to i64 before searching and compare against each item’s numeric ID, matching the existing behavior in Pin, Favorite, and Delete. Update the relevant find predicates and preserve appropriate parse-error handling so inputs such as “01” resolve consistently..github/workflows/release.yml-49-50 (1)
49-50: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winSet
persist-credentials: falseon the checkout step.Same as the CI workflow —
actions/checkout@v4persistsGITHUB_TOKENin.git/configby default. The release workflow doesn't push commits, so disabling persistence is best practice.🔒 Proposed fix
- name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/release.yml around lines 49 - 50, Update the “Checkout” step using actions/checkout@v4 to set persist-credentials: false, matching the CI workflow and preventing GITHUB_TOKEN from being stored in .git/config.Source: Linters/SAST tools
README.md-30-30 (1)
30-30: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winReplace placeholder
YOUR_USERNAMEin the releases URL.The installation section links to
https://github.com/YOUR_USERNAME/openpaste/releases, which is a placeholder. Users following this link will get a 404.📝 Proposed fix
-Download the correct package for your system from the latest [GitHub Releases](https://github.com/YOUR_USERNAME/openpaste/releases). +Download the correct package for your system from the latest [GitHub Releases](https://github.com/coderabbitai/openpaste/releases).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` at line 30, Replace the placeholder owner in the README installation section’s GitHub Releases URL with the repository’s actual GitHub username or organization, ensuring the link points to the valid openpaste releases page.README.md-112-113 (1)
112-113: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse
%LOCALAPPDATA%for the Windows plugins path.
crates/clipboard-daemon/src/main.rsloads plugins fromdirs::data_local_dir().join("openpaste").join("plugins"), so the README should match that Windows location instead of%APPDATA%\openpaste\plugins\.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 112 - 113, Update the Windows plugins path in the README to use `%LOCALAPPDATA%\openpaste\plugins\` instead of `%APPDATA%\openpaste\plugins\`, matching the location derived by `dirs::data_local_dir()` in the plugin-loading logic.apps/cli/src/main.rs-197-200 (1)
197-200: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winWrite image bytes directly (
apps/cli/src/main.rs:197-200)
item.contentalready holds raw bytes from IPC, so base64-decoding here can corrupt image payloads that happen to be valid base64. Writeitem.contentdirectly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cli/src/main.rs` around lines 197 - 200, In the image output handling near the stdout write, remove the STANDARD base64 decode and fallback logic, and write item.content directly with std::io::stdout().write_all(&item.content)?; clean up any now-unused base64 imports.
🧹 Nitpick comments (8)
crates/clipboard-api/src/main.rs (1)
27-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse a proper argument parser instead of manual
std::env::argsparsing.The current parsing doesn't handle
--addr=valuesyntax, provides no--helpoutput, and silently falls through to the env var or default if--addris passed without a value. Addingclapwould provide structured parsing, help text, and validation for free.♻️ Proposed refactor using clap
+use clap::Parser; + +#[derive(Parser)] +#[command(about = "OpenPaste sync relay server")] +struct Args { + /// Address to bind the API server + #[arg(long, env = "OPENPASTE_API_ADDR", default_value = "127.0.0.1:8080")] + addr: SocketAddr, +} #[tokio::main] async fn main() -> Result<()> { + let args = Args::parse(); + // Basic logging tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() .add_directive(tracing::Level::INFO.into()), ) .init(); - // Resolve bind address — flag > env > default - let addr_str = std::env::args() - .skip_while(|a| a != "--addr") - .nth(1) - .or_else(|| std::env::var("OPENPASTE_API_ADDR").ok()) - .unwrap_or_else(|| "127.0.0.1:8080".to_string()); - - let addr: SocketAddr = addr_str - .parse() - .map_err(|e| anyhow::anyhow!("Invalid address '{}': {}", addr_str, e))?; + let addr = args.addr;And add
clap = { workspace = true, features = ["derive", "env"] }toCargo.toml.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-api/src/main.rs` around lines 27 - 31, Replace the manual argument handling in main with a clap-derived argument parser supporting --addr, --addr=value, automatic --help, and validation for missing or invalid values. Configure the addr option to read OPENPASTE_API_ADDR as its environment fallback and retain 127.0.0.1:8080 as the default; add the workspace clap dependency with derive and env features to Cargo.toml.crates/clipboard-sync/src/sync.rs (2)
86-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winGive the HTTP client an explicit timeout.
Client::new()has no request timeout, so a stalled relay server will hangpush/pullindefinitely — and since these run inside thestart_backgroundloop, a single hung request blocks all future sync cycles for that task.⏱️ Suggested change
- http: Client::new(), + http: Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap_or_else(|_| Client::new()),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-sync/src/sync.rs` around lines 86 - 92, Configure an explicit request timeout when constructing the HTTP client in Syncer::new, replacing Client::new() with the builder API and a suitable Duration; ensure the required time import is added. Keep the timeout centralized in the client so push and pull operations cannot block the start_background loop indefinitely.
210-246: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winOne malformed pulled item aborts the entire pull.
The
?on base64 decode (Line 213) and RFC3339 parse (Line 221) propagate an error out of the loop, discarding all remaining (and any already-decoded-but-not-yet-inserted) items. Since insert failures are already tolerated below, prefer skipping the bad item and continuing, mirroring the server-sidesync_pushreject behavior.♻️ Suggested resilience change
- let content = STANDARD - .decode(&sync_item.content_b64) - .map_err(|e| SyncError::SyncFailed(format!("base64 decode: {}", e)))?; + let content = match STANDARD.decode(&sync_item.content_b64) { + Ok(c) => c, + Err(e) => { error!("skip item {}: base64 decode: {}", sync_item.hash, e); continue; } + }; @@ - let created_at = chrono::DateTime::parse_from_rfc3339(&sync_item.created_at) - .map_err(|e| SyncError::SyncFailed(format!("date parse: {}", e)))? - .with_timezone(&chrono::Utc); + let created_at = match chrono::DateTime::parse_from_rfc3339(&sync_item.created_at) { + Ok(dt) => dt.with_timezone(&chrono::Utc), + Err(e) => { error!("skip item {}: date parse: {}", sync_item.hash, e); continue; } + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-sync/src/sync.rs` around lines 210 - 246, Update the pull-item processing loop to skip malformed items instead of returning from the entire pull operation: in the base64 decoding and RFC3339 parsing within the sync method, handle errors by logging or otherwise recording the invalid item and continuing to the next item. Remove the `?` propagation for these per-item conversions while preserving successful item insertion and existing duplicate-insert handling.crates/clipboard-db/src/database.rs (1)
180-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
value_typeis always written as'string'andupdated_atstores epoch-millis into a TEXT column.
value_typeis hardcoded, so the newly added column carries no information — either populate it from the actual value kind or drop it until needed. Separately,strftime('%s','now') * 1000writes a numeric epoch intoupdated_at TEXT, which diverges from the RFC3339 text convention used byclipboard_items.created_at. Not a functional bug (SQLite type affinity coerces it), but the inconsistency will bite anyone parsing timestamps uniformly later.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-db/src/database.rs` around lines 180 - 197, Update upsert_setting to avoid hardcoding value_type as 'string': either derive and persist the actual setting value kind or remove the column usage until supported. Also make updated_at consistent with the RFC3339 timestamp convention used by clipboard_items.created_at, replacing the epoch-millisecond SQLite expression with the project’s standard RFC3339 generation and preserving that format in both insert and conflict-update paths.crates/clipboard-daemon/src/main.rs (1)
51-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDead placeholder key.
_keyis now unused since the vault holdsNoneuntilUnlockVaultderives the real key. Drop it to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/clipboard-daemon/src/main.rs` around lines 51 - 52, Remove the unused placeholder `_key` declaration near the `encryption` initialization in `main`, leaving vault key derivation to `UnlockVault` and retaining only the required encryption state setup.apps/cli/src/main.rs (1)
150-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract repeated IPC error-handling into a helper.
The same three error arms (
Daemon error,IPC error,Unexpected response) are duplicated across all seven command handlers. A small helper that filters error responses would eliminate this repetition.♻️ Proposed helper and usage example
/// Maps IPC transport/daemon errors to `anyhow::Error`, passing through successful responses. fn check_ipc(result: Result<IpcMessage, String>) -> Result<IpcMessage> { match result { Ok(IpcMessage::Error { message }) => Err(anyhow!("Daemon error: {}", message)), Err(e) => Err(anyhow!("IPC error: {}", e)), Ok(other) => Ok(other), } }Usage in each command:
- match c.send(IpcMessage::GetHistory).await { - Ok(IpcMessage::ClipboardHistory { items }) => { /* ... */ } - Ok(IpcMessage::Error { message }) => return Err(anyhow!("Daemon error: {}", message)), - Err(e) => return Err(anyhow!("IPC error: {}", e)), - _ => return Err(anyhow!("Unexpected response")), + match check_ipc(c.send(IpcMessage::GetHistory).await)? { + IpcMessage::ClipboardHistory { items } => { /* ... */ } + _ => return Err(anyhow!("Unexpected response")), }Also applies to: 182-184, 205-207, 220-222, 230-232, 243-245, 256-258, 269-271
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cli/src/main.rs` around lines 150 - 152, Extract the repeated IPC error handling into a shared check_ipc helper near the command handlers, mapping daemon and transport errors to anyhow errors while passing through successful IpcMessage values. Update all seven command handlers, including the branches around the existing handlers, to call check_ipc before matching responses and remove their duplicated Daemon error, IPC error, and Unexpected response arms.apps/desktop/src/App.tsx (1)
246-259: 🚀 Performance & Scalability | 🔵 TrivialN+1 IPC round-trips when loading item tags.
loadAllItemTagsissues oneget_item_tagsinvoke per item, and it runs on everyitemschange (Line 389-394). With auto-refresh replacingitemsfrequently, a list of N entries generates N daemon round-trips each cycle. Consider adding a batched daemon/IPC endpoint (e.g.get_tags_for_items(ids)returning aRecord<id, Tag[]>) and calling it once per refresh to cut this to a single request.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/App.tsx` around lines 246 - 259, Replace the per-item invokes in loadAllItemTags with a single batched get_tags_for_items IPC call accepting all item IDs and returning a Record<number, Tag[]>; update the result handling and error behavior accordingly, and add the corresponding daemon/IPC endpoint and wiring so the existing items-change refresh path performs only one request.apps/desktop/src-tauri/Cargo.toml (1)
14-14: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueDrop the duplicate
global-shortcutfeature
tauri1.5.0 exposes bothglobal-shortcutandglobal-shortcut-all, andglobal-shortcut-allalready includesglobal-shortcut. Remove the extra entry unless it needs to be enabled independently.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src-tauri/Cargo.toml` at line 14, Remove the redundant "global-shortcut" entry from the tauri dependency features list in Cargo.toml, retaining "global-shortcut-all" and all other required features.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Line 31: Update the Intel macOS matrix entry in the release workflow from
macos-13 to macos-15-intel so the x64 release job uses an available
GitHub-hosted runner.
In `@apps/desktop/src/App.tsx`:
- Around line 404-411: Guard the interval setup in the useEffect containing
loadClipboardHistory so a zero or empty settings.refreshInterval cannot create a
busy loop. Clamp the configured value to a sane positive minimum before
multiplying by 1000, while preserving the existing cleanup and dependency
behavior.
- Around line 466-475: Update the handleKeyDown global keyboard handler in the
App useEffect to return immediately whenever document.activeElement is any
editable control, including inputs, textareas, selects, contenteditable
elements, and the existing search input Escape behavior. Preserve search-field
blur/query clearing for Escape while preventing shortcut handling and
preventDefault from affecting all other editable fields.
In `@crates/clipboard-api/src/api.rs`:
- Around line 271-272: Update sync_pull to avoid truncating synchronization at
500 records: preferably pass the since timestamp into the database query used by
list_items, or add pagination so all items newer than since are retrieved and
processed. Ensure the endpoint continues fetching until the complete result set
is synchronized.
- Around line 299-305: Update item_to_json to preserve binary clipboard data:
keep encrypted content masked, return valid UTF-8 content as text, and
base64-encode content that is not valid UTF-8 (or is identified as binary by
content_type). Ensure all regular API endpoints using item_to_json return
lossless content consistent with the sync endpoints.
- Around line 47-58: Add bearer-token authentication middleware to the API
router in router, validating the token sent by sync.rs against configured server
credentials and rejecting missing or invalid tokens before reaching clipboard,
history, item, and sync handlers. Replace CorsLayer::permissive() with an
allowlist of explicitly configured trusted origins, preserving only necessary
methods and headers; ensure configuration is threaded through AppState or router
setup and update the 0.0.0.0 example as appropriate.
- Around line 255-257: Update sync_push’s state.db.insert_item handling to
accept only successful inserts and known UNIQUE-constraint violations, following
the error classification used by set_clipboard. Propagate or return all other
database errors instead of adding their hashes to accepted, while preserving
duplicate items as accepted.
In `@crates/clipboard-platform/src/provider.rs`:
- Around line 76-131: Wrap the entire Objective-C probe in pasteboard_has_image
with objc::rc::autoreleasepool, returning the existing boolean result from its
closure. Keep all Cocoa object access inside the pool so temporary objects are
drained on every poll, while preserving the current null checks and image-type
detection logic.
In `@crates/clipboard-plugin/src/plugin.rs`:
- Around line 219-230: Validate the guest-provided result_len against the
available linear memory before slicing in the transformation flow around
process_fn.call. After handling nonpositive values, compare result_len as a
safely converted usize with memory.data(&store).len(); return an appropriate
PluginError::ExecutionFailed for oversized lengths, and only then create
result_bytes from the bounded slice.
- Around line 52-57: Bound untrusted plugin execution by configuring
interruption support in the constructor `new` and enforcing a per-call limit in
`run_plugin`. Enable Wasmtime fuel or epoch interruption on `Engine`, then reset
and apply the configured budget before each `process_fn.call(...)`, handling
exhaustion as a plugin execution error.
---
Minor comments:
In @.github/workflows/ci.yml:
- Around line 22-23: Update both checkout steps using actions/checkout@v4 in the
CI workflow to set persist-credentials: false, preventing the GITHUB_TOKEN from
being stored in the repository’s Git configuration.
In @.github/workflows/release.yml:
- Around line 49-50: Update the “Checkout” step using actions/checkout@v4 to set
persist-credentials: false, matching the CI workflow and preventing GITHUB_TOKEN
from being stored in .git/config.
In `@apps/cli/src/main.rs`:
- Around line 188-194: Normalize ID handling across the CLI command handlers: in
Get and Copy, parse the user-provided id to i64 before searching and compare
against each item’s numeric ID, matching the existing behavior in Pin, Favorite,
and Delete. Update the relevant find predicates and preserve appropriate
parse-error handling so inputs such as “01” resolve consistently.
- Around line 197-200: In the image output handling near the stdout write,
remove the STANDARD base64 decode and fallback logic, and write item.content
directly with std::io::stdout().write_all(&item.content)?; clean up any
now-unused base64 imports.
In `@crates/clipboard-api/src/api.rs`:
- Around line 95-98: In the item-list response built by the list endpoint,
replace the misleading "total" JSON field derived from out.len() with "count"
(or "returned") to clearly represent the number of items in the current page;
update any related response handling or tests accordingly.
In `@crates/clipboard-core/src/clipboard.rs`:
- Around line 57-69: ClipboardCore::set currently replaces invalid UTF-8 via
String::from_utf8_lossy, silently corrupting clipboard content. Replace it with
std::str::from_utf8 and map validation failures to the appropriate
ClipboardError, then pass the validated string or str reference to ctx.set_text
while preserving existing access-error handling.
In `@crates/clipboard-daemon/src/main.rs`:
- Around line 561-577: Update the SetSyncConfig handler to always persist the
API token setting, using an appropriate empty or cleared value when api_token is
None, so existing tokens can be removed. Include the token upsert result in the
results collection rather than discarding it with let _, ensuring any failure
returns IpcMessage::Error.
- Around line 679-707: Loaded plugins are not applied before captured content is
persisted. In the capture/save pipeline containing the encryption block and
db_item construction, invoke PluginManager::process() on item.content before
encryption, use the processed content for encryption and storage, and handle any
processing errors according to the existing error-handling conventions.
In `@crates/clipboard-db/src/database.rs`:
- Around line 199-246: Update cleanup_old_items and enforce_max_items so
automatic deletion excludes protected rows by adding pinned = 0 AND favorite = 0
to their DELETE conditions, including the enforce_max_items ID-selection
subquery; preserve existing retention and count behavior for unprotected items.
In `@crates/clipboard-sync/src/sync.rs`:
- Around line 98-131: Update SyncService::push to avoid the fixed
list_items(1000, 0) limit: use a database query that applies the since_rfc3339
created_at filter and paginate through all matching records, or repeatedly fetch
batches with an advancing offset/cursor. Ensure every matching item is
transformed into SyncItem and pushed, including older records during a full
sync, while preserving the existing return count and empty-result behavior.
In `@README.md`:
- Line 30: Replace the placeholder owner in the README installation section’s
GitHub Releases URL with the repository’s actual GitHub username or
organization, ensuring the link points to the valid openpaste releases page.
- Around line 112-113: Update the Windows plugins path in the README to use
`%LOCALAPPDATA%\openpaste\plugins\` instead of `%APPDATA%\openpaste\plugins\`,
matching the location derived by `dirs::data_local_dir()` in the plugin-loading
logic.
---
Nitpick comments:
In `@apps/cli/src/main.rs`:
- Around line 150-152: Extract the repeated IPC error handling into a shared
check_ipc helper near the command handlers, mapping daemon and transport errors
to anyhow errors while passing through successful IpcMessage values. Update all
seven command handlers, including the branches around the existing handlers, to
call check_ipc before matching responses and remove their duplicated Daemon
error, IPC error, and Unexpected response arms.
In `@apps/desktop/src-tauri/Cargo.toml`:
- Line 14: Remove the redundant "global-shortcut" entry from the tauri
dependency features list in Cargo.toml, retaining "global-shortcut-all" and all
other required features.
In `@apps/desktop/src/App.tsx`:
- Around line 246-259: Replace the per-item invokes in loadAllItemTags with a
single batched get_tags_for_items IPC call accepting all item IDs and returning
a Record<number, Tag[]>; update the result handling and error behavior
accordingly, and add the corresponding daemon/IPC endpoint and wiring so the
existing items-change refresh path performs only one request.
In `@crates/clipboard-api/src/main.rs`:
- Around line 27-31: Replace the manual argument handling in main with a
clap-derived argument parser supporting --addr, --addr=value, automatic --help,
and validation for missing or invalid values. Configure the addr option to read
OPENPASTE_API_ADDR as its environment fallback and retain 127.0.0.1:8080 as the
default; add the workspace clap dependency with derive and env features to
Cargo.toml.
In `@crates/clipboard-daemon/src/main.rs`:
- Around line 51-52: Remove the unused placeholder `_key` declaration near the
`encryption` initialization in `main`, leaving vault key derivation to
`UnlockVault` and retaining only the required encryption state setup.
In `@crates/clipboard-db/src/database.rs`:
- Around line 180-197: Update upsert_setting to avoid hardcoding value_type as
'string': either derive and persist the actual setting value kind or remove the
column usage until supported. Also make updated_at consistent with the RFC3339
timestamp convention used by clipboard_items.created_at, replacing the
epoch-millisecond SQLite expression with the project’s standard RFC3339
generation and preserving that format in both insert and conflict-update paths.
In `@crates/clipboard-sync/src/sync.rs`:
- Around line 86-92: Configure an explicit request timeout when constructing the
HTTP client in Syncer::new, replacing Client::new() with the builder API and a
suitable Duration; ensure the required time import is added. Keep the timeout
centralized in the client so push and pull operations cannot block the
start_background loop indefinitely.
- Around line 210-246: Update the pull-item processing loop to skip malformed
items instead of returning from the entire pull operation: in the base64
decoding and RFC3339 parsing within the sync method, handle errors by logging or
otherwise recording the invalid item and continuing to the next item. Remove the
`?` propagation for these per-item conversions while preserving successful item
insertion and existing duplicate-insert handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8d32cdf0-d592-4432-8ea0-8fc4da156fca
⛔ Files ignored due to path filters (3)
.github/assets/screenshot1.pngis excluded by!**/*.png.github/assets/screenshot2.pngis excluded by!**/*.pngCargo.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
.github/workflows/ci.yml.github/workflows/release.ymlCHANGELOG.mdREADME.mdapps/cli/Cargo.tomlapps/cli/src/main.rsapps/desktop/src-tauri/Cargo.tomlapps/desktop/src-tauri/src/main.rsapps/desktop/src-tauri/tauri.conf.jsonapps/desktop/src/App.tsxapps/desktop/src/index.csscrates/clipboard-api/Cargo.tomlcrates/clipboard-api/src/api.rscrates/clipboard-api/src/main.rscrates/clipboard-core/Cargo.tomlcrates/clipboard-core/src/clipboard.rscrates/clipboard-daemon/Cargo.tomlcrates/clipboard-daemon/src/main.rscrates/clipboard-db/migrations/001_initial.sqlcrates/clipboard-db/src/database.rscrates/clipboard-db/src/models.rscrates/clipboard-encryption/src/encryption.rscrates/clipboard-encryption/src/key_derivation.rscrates/clipboard-ipc/src/ipc.rscrates/clipboard-ipc/src/lib.rscrates/clipboard-platform/Cargo.tomlcrates/clipboard-platform/src/provider.rscrates/clipboard-plugin/src/plugin.rscrates/clipboard-sync/Cargo.tomlcrates/clipboard-sync/src/lib.rscrates/clipboard-sync/src/sync.rs
| tauri_args: --target aarch64-apple-darwin | ||
|
|
||
| # macOS Intel | ||
| - os: macos-13 |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check GitHub Actions runner availability for macos-13
# Also verify the latest available Intel macOS runner labels
gh api /repos/{owner}/{repo}/actions/runners --paginate 2>/dev/null | \
jq '.runners[].labels[].name' 2>/dev/null | sort -u || \
echo "Unable to query runners — verify manually at https://github.com/actions/runner-images"Repository: anonfedora/openpaste
Length of output: 158
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the workflow and inspect the relevant job section
ast-grep outline .github/workflows/release.yml --view expanded || true
echo '---'
cat -n .github/workflows/release.yml | sed -n '1,140p'
echo '--- runner label references ---'
rg -n 'macos-13|macos-15-intel|macOS-x64|x86_64-apple-darwin' .github/workflows .github || trueRepository: anonfedora/openpaste
Length of output: 5289
🌐 Web query:
GitHub Actions macOS runner labels macos-13 deprecation current availability macos-15-intel docs
💡 Result:
The macOS 13 GitHub Actions runner image has been fully retired since December 4, 2025 [1][2]. Any workflows pinned to the macos-13 label will fail immediately [2][3]. Regarding the macos-15-intel runner: - Availability: It is currently available as a GitHub-hosted runner [4][5]. - Purpose: It provides an x86_64 (Intel) environment on macOS 15, intended as a replacement for users migrating from the deprecated macos-13 runners [5]. - Support Lifecycle: This runner is scheduled for retirement in August 2027 [2][4][5]. GitHub has stated that after this date, the x86_64 architecture will no longer be supported for GitHub-hosted macOS runners [5]. For workflows requiring Intel-based macOS runners, macos-15-intel is the designated long-term solution until its scheduled retirement [5][6]. Users are otherwise encouraged to migrate to Apple Silicon (ARM64) runners, such as macos-14 or macos-15, where possible [7][2].
Citations:
- 1: https://github.blog/changelog/2025-09-19-github-actions-macos-13-runner-image-is-closing-down/
- 2: https://pocketlantern.dev/briefs/github-actions-macos-13-runner-retirement-october-2025
- 3: [macOS] The macOS 13 Ventura based runner images will begin deprecation on September 22nd and will be fully unsupported by December 4th for GitHub and ADO actions/runner-images#13046
- 4: [macOS] The additional macOS 15 Sonoma Intel-based image will be available in GitHub Actions actions/runner-images#13045
- 5: [macOS] The additional macOS 15 Sonoma Intel-based image will be available in GitHub Actions actions/runner-images#13045
- 6: Label macos-14-intel is missing actions/runner-images#13164
- 7: https://github.blog/changelog/2025-07-11-upcoming-changes-to-macos-hosted-runners-macos-latest-migration-and-xcode-support-policy-updates/
Replace macos-13 with macos-15-intel for the Intel macOS build
macos-13 is retired, so this matrix entry won’t get a GitHub-hosted runner and the x64 release job will fail to start.
🧰 Tools
🪛 actionlint (1.7.12)
[error] 31-31: label "macos-13" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-intel", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file
(runner-label)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml at line 31, Update the Intel macOS matrix
entry in the release workflow from macos-13 to macos-15-intel so the x64 release
job uses an available GitHub-hosted runner.
Source: Linters/SAST tools
| useEffect(() => { | ||
| const interval = setInterval(() => { | ||
| if (!searchQuery && activeTagFilter === null) { | ||
| loadClipboardHistory(false) | ||
| } | ||
| }, settings.refreshInterval * 1000) | ||
| return () => clearInterval(interval) | ||
| }, [searchQuery, settings.refreshInterval, activeTagFilter]) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Guard against a zero (or empty) refresh interval.
settings.refreshInterval is user-editable via parseInt(e.target.value) || 0 (Line 1715), so it can become 0. setInterval(fn, 0) then fires as fast as the event loop allows, turning auto-refresh into a busy loop that continuously hits get_clipboard_history over IPC. Clamp to a sane minimum.
🔧 Proposed fix
useEffect(() => {
+ const intervalMs = Math.max(1, settings.refreshInterval) * 1000
const interval = setInterval(() => {
if (!searchQuery && activeTagFilter === null) {
loadClipboardHistory(false)
}
- }, settings.refreshInterval * 1000)
+ }, intervalMs)
return () => clearInterval(interval)
}, [searchQuery, settings.refreshInterval, activeTagFilter])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| const interval = setInterval(() => { | |
| if (!searchQuery && activeTagFilter === null) { | |
| loadClipboardHistory(false) | |
| } | |
| }, settings.refreshInterval * 1000) | |
| return () => clearInterval(interval) | |
| }, [searchQuery, settings.refreshInterval, activeTagFilter]) | |
| useEffect(() => { | |
| const intervalMs = Math.max(1, settings.refreshInterval) * 1000 | |
| const interval = setInterval(() => { | |
| if (!searchQuery && activeTagFilter === null) { | |
| loadClipboardHistory(false) | |
| } | |
| }, intervalMs) | |
| return () => clearInterval(interval) | |
| }, [searchQuery, settings.refreshInterval, activeTagFilter]) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/App.tsx` around lines 404 - 411, Guard the interval setup in
the useEffect containing loadClipboardHistory so a zero or empty
settings.refreshInterval cannot create a busy loop. Clamp the configured value
to a sane positive minimum before multiplying by 1000, while preserving the
existing cleanup and dependency behavior.
| useEffect(() => { | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| // Don't handle if typing in search input | ||
| if (document.activeElement === searchInputRef.current) { | ||
| if (e.key === 'Escape') { | ||
| searchInputRef.current?.blur() | ||
| setSearchQuery('') | ||
| } | ||
| return | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Global keyboard handler hijacks typing in every input except the search box.
The guard only bypasses when document.activeElement === searchInputRef.current. When the user types in the tag-name input, master/unlock password fields, sync server/token inputs, or the numeric settings inputs, this global handler still runs and calls e.preventDefault() for j/k/p/f/d/Enter/arrows — so those characters never reach the field and instead fire list actions (pin/favorite/delete/copy) on the hovered item.
Gate the entire handler on whether focus is in any editable element.
🔧 Proposed fix
const handleKeyDown = (e: KeyboardEvent) => {
+ // Don't handle shortcuts while typing in any editable field
+ const el = document.activeElement as HTMLElement | null
+ const isEditable =
+ el instanceof HTMLInputElement ||
+ el instanceof HTMLTextAreaElement ||
+ (el?.isContentEditable ?? false)
// Don't handle if typing in search input
if (document.activeElement === searchInputRef.current) {
if (e.key === 'Escape') {
searchInputRef.current?.blur()
setSearchQuery('')
}
return
}
+ if (isEditable) return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| // Don't handle if typing in search input | |
| if (document.activeElement === searchInputRef.current) { | |
| if (e.key === 'Escape') { | |
| searchInputRef.current?.blur() | |
| setSearchQuery('') | |
| } | |
| return | |
| } | |
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| // Don't handle shortcuts while typing in any editable field | |
| const el = document.activeElement as HTMLElement | null | |
| const isEditable = | |
| el instanceof HTMLInputElement || | |
| el instanceof HTMLTextAreaElement || | |
| (el?.isContentEditable ?? false) | |
| // Don't handle if typing in search input | |
| if (document.activeElement === searchInputRef.current) { | |
| if (e.key === 'Escape') { | |
| searchInputRef.current?.blur() | |
| setSearchQuery('') | |
| } | |
| return | |
| } | |
| if (isEditable) return |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/App.tsx` around lines 466 - 475, Update the handleKeyDown
global keyboard handler in the App useEffect to return immediately whenever
document.activeElement is any editable control, including inputs, textareas,
selects, contenteditable elements, and the existing search input Escape
behavior. Preserve search-field blur/query clearing for Escape while preventing
shortcut handling and preventDefault from affecting all other editable fields.
| fn router(state: AppState) -> Router { | ||
| Router::new() | ||
| .route("/api/v1/status", get(handlers::status)) | ||
| .route( | ||
| "/api/v1/clipboard", | ||
| get(handlers::get_clipboard).post(handlers::set_clipboard), | ||
| ) | ||
| .route("/api/v1/search", post(handlers::search)) | ||
| .route("/api/v1/status", get(status)) | ||
| .route("/api/v1/clipboard", get(get_clipboard).post(set_clipboard)) | ||
| .route("/api/v1/search", post(search)) | ||
| .route("/api/v1/history", get(list_history)) | ||
| .route("/api/v1/item/:id", get(get_item).delete(delete_item)) | ||
| .route("/api/v1/sync/push", post(sync_push)) | ||
| .route("/api/v1/sync/pull", get(sync_pull)) | ||
| .with_state(state) | ||
| .layer(CorsLayer::permissive()) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
No authentication and permissive CORS on all API endpoints.
The router exposes every endpoint without any auth middleware, and CorsLayer::permissive() allows any origin. The sync client (crates/clipboard-sync/src/sync.rs) sends bearer_auth(token) when configured, but the server never validates it. The usage example in main.rs shows --addr 0.0.0.0:8080, meaning the API can be network-exposed. Anyone who can reach the server can read clipboard history (potentially containing passwords or PII), inject content, delete items, and push/pull sync data.
At minimum, add a bearer-token extraction layer and restrict CORS to known origins.
🔒 Suggested auth middleware using Axum middleware + tower-http
use axum::{
extract::{Path, Query, State},
routing::{get, post},
Json, Router,
};
+use axum::http::{header::AUTHORIZATION, HeaderMap, StatusCode};
+use axum::middleware::{self, Next};
+use axum::response::Response;
+async fn require_auth(
+ headers: HeaderMap,
+ State(state): State<AppState>,
+ next: Next,
+) -> Result<Response, StatusCode> {
+ let expected = std::env::var("OPENPASTE_API_TOKEN").unwrap_or_default();
+ if expected.is_empty() {
+ return Ok(next.run(state).await); // No token configured = open mode
+ }
+ let token = headers
+ .get(AUTHORIZATION)
+ .and_then(|v| v.to_str().ok())
+ .and_then(|v| v.strip_prefix("Bearer "))
+ .unwrap_or("");
+ if token == expected {
+ Ok(next.run(state).await)
+ } else {
+ Err(StatusCode::UNAUTHORIZED)
+ }
+}
fn router(state: AppState) -> Router {
Router::new()
.route("/api/v1/status", get(status))
.route("/api/v1/clipboard", get(get_clipboard).post(set_clipboard))
.route("/api/v1/search", post(search))
.route("/api/v1/history", get(list_history))
.route("/api/v1/item/:id", get(get_item).delete(delete_item))
.route("/api/v1/sync/push", post(sync_push))
.route("/api/v1/sync/pull", get(sync_pull))
.with_state(state)
- .layer(CorsLayer::permissive())
+ .layer(middleware::from_fn_with_state(state.clone(), require_auth))
+ .layer(CorsLayer::new()
+ .allow_origin([axum::http::HeaderValue::from_static("http://localhost:1420")])
+ .allow_methods(Any)
+ .allow_headers(Any))
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-api/src/api.rs` around lines 47 - 58, Add bearer-token
authentication middleware to the API router in router, validating the token sent
by sync.rs against configured server credentials and rejecting missing or
invalid tokens before reaching clipboard, history, item, and sync handlers.
Replace CorsLayer::permissive() with an allowlist of explicitly configured
trusted origins, preserving only necessary methods and headers; ensure
configuration is threaded through AppState or router setup and update the
0.0.0.0 example as appropriate.
| match state.db.insert_item(&item).await { | ||
| Ok(_) | Err(_) => accepted.push(wire.hash), // duplicates are also "accepted" | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
sync_push silently swallows all database errors as "accepted".
Ok(_) | Err(_) treats every insert outcome — including disk full, corruption, or connection failures — as accepted. The comment says "duplicates are also accepted", but the code goes far beyond that. The client will believe items were synced when they may not have been inserted, causing silent data loss.
The set_clipboard handler (line 144) correctly distinguishes UNIQUE violations from other errors. Apply the same pattern here.
🐛 Proposed fix to distinguish duplicates from real errors
- match state.db.insert_item(&item).await {
- Ok(_) | Err(_) => accepted.push(wire.hash), // duplicates are also "accepted"
- }
+ match state.db.insert_item(&item).await {
+ Ok(_) => accepted.push(wire.hash),
+ Err(e) if e.to_string().contains("UNIQUE") => accepted.push(wire.hash),
+ Err(_) => rejected.push(wire.hash),
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| match state.db.insert_item(&item).await { | |
| Ok(_) | Err(_) => accepted.push(wire.hash), // duplicates are also "accepted" | |
| } | |
| match state.db.insert_item(&item).await { | |
| Ok(_) => accepted.push(wire.hash), | |
| Err(e) if e.to_string().contains("UNIQUE") => accepted.push(wire.hash), | |
| Err(_) => rejected.push(wire.hash), | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-api/src/api.rs` around lines 255 - 257, Update sync_push’s
state.db.insert_item handling to accept only successful inserts and known
UNIQUE-constraint violations, following the error classification used by
set_clipboard. Propagate or return all other database errors instead of adding
their hashes to accepted, while preserving duplicate items as accepted.
| // Fetch recent items (server just returns last 500; client filters by since) | ||
| let items = match state.db.list_items(500, 0).await { |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
sync_pull hardcodes a 500-item limit, causing silent data loss for larger histories.
list_items(500, 0) returns only the 500 most recent items. If more than 500 items were created after the client's since timestamp, older items are never returned and never synced. There is no pagination mechanism on this endpoint.
Consider filtering by since server-side in the database query, or adding pagination support to the sync pull endpoint.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-api/src/api.rs` around lines 271 - 272, Update sync_pull to
avoid truncating synchronization at 500 records: preferably pass the since
timestamp into the database query used by list_items, or add pagination so all
items newer than since are retrieved and processed. Ensure the endpoint
continues fetching until the complete result set is synchronized.
| fn item_to_json(item: &clipboard_db::models::ClipboardItem) -> Value { | ||
| // Return text content as UTF-8 string; binary/encrypted as base64 | ||
| let content_str = if item.encrypted { | ||
| "[encrypted]".to_string() | ||
| } else { | ||
| String::from_utf8_lossy(&item.content).to_string() | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Binary content (e.g., images) is corrupted by from_utf8_lossy in item_to_json.
Non-encrypted binary clipboard content (images, which this PR adds support for) will produce replacement characters and data loss when passed through String::from_utf8_lossy. The content_type field may indicate "image/png", but the content field will contain garbage. The sync endpoints handle this correctly by base64-encoding, but the regular API endpoints (get_clipboard, list_history, get_item, search) do not.
Check whether the content is valid UTF-8 and base64-encode if not, or gate on content_type.
💚 Proposed fix to base64-encode non-UTF-8 content
fn item_to_json(item: &clipboard_db::models::ClipboardItem) -> Value {
- // Return text content as UTF-8 string; binary/encrypted as base64
- let content_str = if item.encrypted {
- "[encrypted]".to_string()
- } else {
- String::from_utf8_lossy(&item.content).to_string()
- };
+ use base64::{engine::general_purpose::STANDARD, Engine as _};
+
+ let content_str = if item.encrypted {
+ "[encrypted]".to_string()
+ } else {
+ match std::str::from_utf8(&item.content) {
+ Ok(s) => s.to_string(),
+ Err(_) => format!("data:base64,{}", STANDARD.encode(&item.content)),
+ }
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn item_to_json(item: &clipboard_db::models::ClipboardItem) -> Value { | |
| // Return text content as UTF-8 string; binary/encrypted as base64 | |
| let content_str = if item.encrypted { | |
| "[encrypted]".to_string() | |
| } else { | |
| String::from_utf8_lossy(&item.content).to_string() | |
| }; | |
| fn item_to_json(item: &clipboard_db::models::ClipboardItem) -> Value { | |
| use base64::{engine::general_purpose::STANDARD, Engine as _}; | |
| let content_str = if item.encrypted { | |
| "[encrypted]".to_string() | |
| } else { | |
| match std::str::from_utf8(&item.content) { | |
| Ok(s) => s.to_string(), | |
| Err(_) => format!("data:base64,{}", STANDARD.encode(&item.content)), | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-api/src/api.rs` around lines 299 - 305, Update item_to_json
to preserve binary clipboard data: keep encrypted content masked, return valid
UTF-8 content as text, and base64-encode content that is not valid UTF-8 (or is
identified as binary by content_type). Ensure all regular API endpoints using
item_to_json return lossless content consistent with the sync endpoints.
| /// On macOS, check whether the pasteboard currently advertises an image type, | ||
| /// without actually reading the image data. This avoids the ~50 ms arboard | ||
| /// penalty on every poll when the clipboard contains only text. | ||
| /// | ||
| /// Returns `true` → image type is present, go ahead and call `get_image()`. | ||
| /// Returns `false` → skip image attempt entirely. | ||
| #[cfg(target_os = "macos")] | ||
| #[allow(unexpected_cfgs)] | ||
| fn pasteboard_has_image() -> bool { | ||
| // Use the Objective-C runtime to call | ||
| // [[NSPasteboard generalPasteboard] canReadObjectForClasses:...] or | ||
| // more simply check the types array for common image UTIs. | ||
| // | ||
| // We do this via a tiny inline Objective-C call using the `objc` crate | ||
| // that is already a transitive dependency. | ||
| use std::ffi::CStr; | ||
| unsafe { | ||
| use objc::runtime::{Class, Object}; | ||
| use objc::{msg_send, sel, sel_impl}; | ||
|
|
||
| let cls = match Class::get("NSPasteboard") { | ||
| Some(c) => c, | ||
| None => return false, | ||
| }; | ||
| let pb: *mut Object = msg_send![cls, generalPasteboard]; | ||
| if pb.is_null() { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl ClipboardProvider for WindowsProvider { | ||
| async fn get_content(&self) -> Result<ClipboardItem, PlatformError> { | ||
| let mut ctx = | ||
| Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; | ||
|
|
||
| let text = ctx | ||
| .get_text() | ||
| .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; | ||
|
|
||
| let content_bytes = text.into_bytes(); | ||
| let content_type = ContentType::detect(&content_bytes); | ||
|
|
||
| Ok(ClipboardItem::new(content_type, content_bytes)) | ||
| let types: *mut Object = msg_send![pb, types]; | ||
| if types.is_null() { | ||
| return false; | ||
| } | ||
|
|
||
| async fn set_content(&self, item: &ClipboardItem) -> Result<(), PlatformError> { | ||
| if item.content_type == ContentType::Text { | ||
| let text = String::from_utf8_lossy(&item.content).to_string(); | ||
| let mut ctx = | ||
| Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; | ||
|
|
||
| ctx.set_text(&text) | ||
| .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; | ||
| let count: usize = msg_send![types, count]; | ||
| for i in 0..count { | ||
| let type_str: *mut Object = msg_send![types, objectAtIndex: i]; | ||
| if type_str.is_null() { | ||
| continue; | ||
| } | ||
| let utf8: *const std::os::raw::c_char = msg_send![type_str, UTF8String]; | ||
| if utf8.is_null() { | ||
| continue; | ||
| } | ||
| let s = CStr::from_ptr(utf8).to_string_lossy(); | ||
| // Common image UTIs advertised by macOS apps | ||
| if s.contains("image") | ||
| || s == "public.png" | ||
| || s == "public.jpeg" | ||
| || s == "public.tiff" | ||
| || s == "com.apple.pict" | ||
| { | ||
| return true; | ||
| } | ||
| // TODO: Handle other content types | ||
| Ok(()) | ||
| } | ||
|
|
||
| async fn watch_changes(&self) -> Result<(), PlatformError> { | ||
| // TODO: Implement Windows clipboard watching using clipboard format listeners | ||
| Err(PlatformError::WatchFailed("Not implemented".to_string())) | ||
| } | ||
| false | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if objc is declared as a direct dependency and verify autoreleasepool availability
ast-grep outline crates/clipboard-platform/Cargo.toml --type toml 2>/dev/null || true
rg -n 'objc' crates/clipboard-platform/Cargo.toml
# Also check if objc::rc::autoreleasepool is used elsewhere in the codebase
rg -n 'autoreleasepool' --type rust crates/Repository: anonfedora/openpaste
Length of output: 174
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== clipboard-platform Cargo.toml =="
sed -n '1,120p' crates/clipboard-platform/Cargo.toml
echo
echo "== objc dependency in lockfile =="
rg -n '^name = "objc"|^version = ".*"$' Cargo.lock -n -A2 -B1 | sed -n '1,40p'
echo
echo "== autoreleasepool usage in repo =="
rg -n 'objc::rc::autoreleasepool|autoreleasepool\(' crates/ -g '*.rs'Repository: anonfedora/openpaste
Length of output: 2605
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate objc crate sources =="
python3 - <<'PY'
import glob, os
cands = glob.glob(os.path.expanduser('~/.cargo/registry/src/*/objc-*'))
print("\n".join(cands[:20]))
PY
echo
echo "== search for autoreleasepool in objc sources =="
python3 - <<'PY'
import glob, os, subprocess, shlex
for root in glob.glob(os.path.expanduser('~/.cargo/registry/src/*/objc-*')):
print(f"-- {root} --")
subprocess.run(["bash","-lc",f"rg -n 'autoreleasepool|pub mod rc|mod rc' {shlex.quote(root)}"], check=False)
PY
echo
echo "== inspect relevant objc source files if present =="
python3 - <<'PY'
import glob, os, subprocess, shlex, pathlib
roots = glob.glob(os.path.expanduser('~/.cargo/registry/src/*/objc-*'))
for root in roots[:3]:
for path in pathlib.Path(root).rglob("*.rs"):
if path.name in {"lib.rs", "rc.rs"}:
print(f"\n-- {path} --")
subprocess.run(["bash","-lc",f"sed -n '1,260p' {shlex.quote(str(path))}"], check=False)
PYRepository: anonfedora/openpaste
Length of output: 294
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== crates/clipboard-platform/src/provider.rs =="
sed -n '1,220p' crates/clipboard-platform/src/provider.rs
echo
echo "== search for pasteboard_has_image callers =="
rg -n 'pasteboard_has_image|get_image\(' crates/clipboard-platform/src -g '*.rs'
echo
echo "== search for any objc / cocoa autorelease handling =="
rg -n 'autoreleasepool|NSAutoreleasePool|NSApplication|runloop|main thread' crates/ -g '*.rs'Repository: anonfedora/openpaste
Length of output: 8602
🌐 Web query:
docs.rs objc 0.2 autoreleasepool objc::rc::autoreleasepool
💡 Result:
In the objc crate (version 0.2), objc::rc::autoreleasepool is a function used to execute code within the context of a new Objective-C autorelease pool [1][2]. It provides a Rust-safe wrapper for the @autoreleasepool blocks found in Objective-C and Swift [1][3]. The function signature is defined as [1]: pub fn autoreleasepool<T, F: FnOnce -> T>(f: F) -> T When called, autoreleasepool creates a new autorelease pool, executes the provided closure f, and drains the pool immediately after the closure completes [1][2]. This is essential for managing the memory of Objective-C objects that have been marked for autorelease—preventing memory accumulation by ensuring they are released at the end of the specified scope rather than waiting for the thread's main pool to drain [4][5]. Key points regarding its usage: - ARC-like Semantics: The rc module provides utilities like StrongPtr and WeakPtr to manage reference counting [4][2]. When you call .autorelease() on a StrongPtr inside an autoreleasepool block, the ownership of that object is effectively transferred to the pool, and it will be released when the pool drains [4][5]. - Relationship to objc2: The objc crate has been largely succeeded by the objc2 crate [6][7]. In objc2, the autoreleasepool function has been updated to accept the pool as a parameter, allowing for safer management of lifetimes for objects tied to the pool [8][7]. If you are working on modern projects, it is recommended to use objc2 for better safety guarantees and updated APIs [7].
Citations:
- 1: https://docs.rs/objc/latest/objc/rc/fn.autoreleasepool.html
- 2: https://docs.rs/objc/latest/objc/rc/index.html
- 3: http://sasheldon.com/rust-objc/objc/rc/fn.autoreleasepool.html
- 4: https://docs.rs/crate/objc/0.2.7
- 5: https://github.com/SSheldon/rust-objc/blob/master/README.md
- 6: https://docs.rs/crate/objc2/0.2.7
- 7: https://docs.rs/objc2/0.3.0-beta.3/objc2/rc/index.html
- 8: https://docs.rs/objc2/latest/objc2/rc/fn.autoreleasepool.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== find macOS entrypoints and provider construction =="
rg -n 'PlatformProvider::Macos|PlatformProvider|watch_changes|get_content|read_clipboard_item|tokio::main|main\(' crates/ -g '*.rs'
echo
echo "== inspect crate roots / binaries =="
git ls-files | rg '(^|/)(main|lib)\.rs$|Cargo\.toml$'
echo
echo "== search for any explicit autorelease pool setup in macOS-related code =="
rg -n 'autoreleasepool|NSAutoreleasePool|cocoa::app|NSApplication|NSApp' crates/ -g '*.rs'Repository: anonfedora/openpaste
Length of output: 3174
Wrap the macOS pasteboard probe in an autorelease pool crates/clipboard-platform/src/provider.rs:84 — objc::rc::autoreleasepool is available in the declared objc = "0.2" dependency, and this path runs on every poll; drain the Cocoa temporaries here to avoid unbounded growth in the daemon.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-platform/src/provider.rs` around lines 76 - 131, Wrap the
entire Objective-C probe in pasteboard_has_image with objc::rc::autoreleasepool,
returning the existing boolean result from its closure. Keep all Cocoa object
access inside the pool so temporary objects are drained on every poll, while
preserving the current null checks and image-type detection logic.
| pub fn new() -> Self { | ||
| Self {} | ||
| Self { | ||
| engine: Engine::default(), | ||
| plugins: Arc::new(Mutex::new(HashMap::new())), | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
wasmtime consume_fuel epoch_interruption set deadline example
💡 Result:
In Wasmtime, fuel and epoch interruption are two distinct mechanisms used to interrupt or limit the execution of WebAssembly code [1][2]. They are often configured separately, though they can be used together [2]. Fuel Consumption Fuel is a deterministic mechanism where every Wasm operation consumes a specific amount of fuel [1][3]. Once a store runs out of fuel, execution traps [1][3]. 1. Configuration: Enable fuel in your engine config using config.consume_fuel(true) [2]. 2. Initialization: Set an initial fuel amount in your store using store.set_fuel(amount)? [2]. 3. Usage: You can check remaining fuel or replenish it during execution using store.get_fuel and store.add_fuel(amount)? [2]. Epoch Interruption Epoch interruption is a cooperative, timer-based mechanism that is generally more performant but coarser-grained than fuel [1][2]. The host increments an epoch counter, and Wasmtime checks this at safe points [2]. 1. Configuration: Enable epoch interruption in your config using config.epoch_interruption(true) [4][2]. 2. Deadline: Use store.set_epoch_deadline(value) to set when the next trap should occur relative to the engine's current epoch [4][2]. 3. Trigger: Call engine.increment_epoch from a host thread to advance the epoch, which will trigger a trap in the guest if the deadline has been reached [4][2]. Example Summary To implement these, you typically configure the engine, create a store, and set the initial limits [4][2]: let mut config = Config::new; config.consume_fuel(true); config.epoch_interruption(true); let engine = Engine::new(&config)?; let mut store = Store::new(&engine, ); store.set_fuel(10_000)?; // Fuel-based limit store.set_epoch_deadline(1)?; // Epoch-based limit For asynchronous environments, you can also use store.fuel_async_yield_interval or store.epoch_deadline_async_yield_and_update to yield control back to the executor instead of trapping [1][5].
Citations:
- 1: https://docs.wasmtime.dev/api/wasmtime/
- 2: https://www.systemshardening.com/articles/wasm/wasmtime-production-hardening/
- 3: https://docs.rs/crate/wasmtime-cli/latest/source/docs/examples-interrupting-wasm.md
- 4: https://github.com/bytecodealliance/wasmtime/blob/main/examples/epochs.rs
- 5: https://docs.rs/wasmtime/latest/wasmtime/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file outline ==\n'
ast-grep outline crates/clipboard-plugin/src/plugin.rs --view expanded || true
printf '\n== relevant file section ==\n'
nl -ba crates/clipboard-plugin/src/plugin.rs | sed -n '1,260p'
printf '\n== search for engine construction and execution path ==\n'
rg -n "Engine::default|Config::new|consume_fuel|epoch_interruption|set_epoch_deadline|set_fuel|process_item|clipboard-plugin" crates -g '!**/target/**' || trueRepository: anonfedora/openpaste
Length of output: 947
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== plugin.rs (lines 1-260) ==\n'
sed -n '1,260p' crates/clipboard-plugin/src/plugin.rs
printf '\n== wasmtime config/search in clipboard-plugin ==\n'
rg -n "Config::new|consume_fuel|epoch_interruption|set_epoch_deadline|set_fuel|Engine::default|Engine::new|Store::new|Linker::new|call_async|call" crates/clipboard-plugin -g '!**/target/**' || trueRepository: anonfedora/openpaste
Length of output: 9529
Bound untrusted plugin execution Engine::default() leaves process_fn.call(...) unbounded, so a plugin with an infinite process_item loop can block the caller thread indefinitely. Configure fuel or epoch interruption and apply a per-call limit in run_plugin.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-plugin/src/plugin.rs` around lines 52 - 57, Bound untrusted
plugin execution by configuring interruption support in the constructor `new`
and enforcing a per-call limit in `run_plugin`. Enable Wasmtime fuel or epoch
interruption on `Engine`, then reset and apply the configured budget before each
`process_fn.call(...)`, handling exhaustion as a plugin execution error.
| let result_len = process_fn | ||
| .call(&mut store, (0, content_bytes.len() as i32)) | ||
| .map_err(|e| PluginError::ExecutionFailed(format!("call: {}", e)))?; | ||
|
|
||
| if result_len <= 0 { | ||
| return Ok(None); // no transformation | ||
| } | ||
|
|
||
| // Read back the result from memory offset 0 | ||
| let result_bytes: Vec<u8> = memory.data(&store)[..result_len as usize].to_vec(); | ||
| let result = String::from_utf8(result_bytes) | ||
| .map_err(|e| PluginError::ExecutionFailed(format!("utf8: {}", e)))?; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Untrusted result_len can panic the host via out-of-bounds slice.
result_len is returned by the guest WASM and only checked for <= 0. If a plugin returns a length larger than its linear memory, memory.data(&store)[..result_len as usize] panics and takes down the caller. Bound it against the memory length before slicing.
🛡️ Proposed fix
if result_len <= 0 {
return Ok(None); // no transformation
}
- // Read back the result from memory offset 0
- let result_bytes: Vec<u8> = memory.data(&store)[..result_len as usize].to_vec();
+ // Read back the result from memory offset 0
+ let data = memory.data(&store);
+ let end = result_len as usize;
+ if end > data.len() {
+ return Err(PluginError::ExecutionFailed(
+ "Plugin returned length exceeding guest memory".to_string(),
+ ));
+ }
+ let result_bytes: Vec<u8> = data[..end].to_vec();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let result_len = process_fn | |
| .call(&mut store, (0, content_bytes.len() as i32)) | |
| .map_err(|e| PluginError::ExecutionFailed(format!("call: {}", e)))?; | |
| if result_len <= 0 { | |
| return Ok(None); // no transformation | |
| } | |
| // Read back the result from memory offset 0 | |
| let result_bytes: Vec<u8> = memory.data(&store)[..result_len as usize].to_vec(); | |
| let result = String::from_utf8(result_bytes) | |
| .map_err(|e| PluginError::ExecutionFailed(format!("utf8: {}", e)))?; | |
| let result_len = process_fn | |
| .call(&mut store, (0, content_bytes.len() as i32)) | |
| .map_err(|e| PluginError::ExecutionFailed(format!("call: {}", e)))?; | |
| if result_len <= 0 { | |
| return Ok(None); // no transformation | |
| } | |
| // Read back the result from memory offset 0 | |
| let data = memory.data(&store); | |
| let end = result_len as usize; | |
| if end > data.len() { | |
| return Err(PluginError::ExecutionFailed( | |
| "Plugin returned length exceeding guest memory".to_string(), | |
| )); | |
| } | |
| let result_bytes: Vec<u8> = data[..end].to_vec(); | |
| let result = String::from_utf8(result_bytes) | |
| .map_err(|e| PluginError::ExecutionFailed(format!("utf8: {}", e)))?; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/clipboard-plugin/src/plugin.rs` around lines 219 - 230, Validate the
guest-provided result_len against the available linear memory before slicing in
the transformation flow around process_fn.call. After handling nonpositive
values, compare result_len as a safely converted usize with
memory.data(&store).len(); return an appropriate PluginError::ExecutionFailed
for oversized lengths, and only then create result_bytes from the bounded slice.
Title:
feat: plugins, sync UI, daemon sidecar, tests, release pipelineSummary
This PR completes the remaining feature work and infrastructure needed for a v0.1.0 release. It closes the gaps between what the app claimed to support and what was actually wired up.
What's changed
WASM Plugin System
clipboard-plugincrate:PluginManagerusingwasmtime— load/unload.wasmfiles, run them against text clipboard items in sequence.wasmfiles from<data-dir>/plugins/on startupListPlugins,LoadPlugin,UnloadPlugin— fully handled in daemonlist_plugins,load_plugin,unload_plugin.wasmfilter), lists loaded plugins with name/path/status badge, unload button per plugin, refreshCross-device Sync
clipboard-synccrate:SyncClientwithpush(since),pull(since),sync_once(),start_background()— push/pull over HTTP against any server running the OpenPaste APIclipboard-apicrate: now has a runnable binary (cargo run --bin clipboard-api) with--addrflag andOPENPASTE_API_ADDRenv var; shares the daemon's SQLite databaseGetSyncConfig,SetSyncConfig,SyncNow— fully handled in daemonget_sync_config,set_sync_config,sync_nowDaemon Sidecar
target/debug/for dev)Test Coverage
clipboard-db: 13 integration tests usingDatabase::in_memory()— insert/get/delete, pin/fav, dedup (UNIQUE constraint), settings upsert, tag CRUD, item-tag assignment, tag filter, FTS5 search, cleanup by age, max-items enforcementclipboard-encryption: 12 unit tests — encrypt/decrypt roundtrip, nonce uniqueness, wrong key rejection, AEAD tamper detection, empty/large input, key derivation determinism, verify correct/wrong passwordClipboardManager— no longer a stubclipboard-core'sClipboardManager::capture()andset()now implemented usingarboarddirectly; includes max-size enforcement and image type rejection with a clear errorCode quality
clipboard-plugin(Instance,Memory,error) andclipboard-api(deleterouting import)FnOnceclosure bug inclipboard-plugin's wasmtimefunc_wrap(was movingplugin_nameout, making it non-Fn)base64dep toclipboard-sync,uuid+chronotoclipboard-daemonCI / Release pipeline
ci.yml): modernized todtolnay/rust-toolchain+Swatinem/rust-cache; split intotest(cargo test + clippy + fmt, workspace minus desktop) andbuild-desktop(cargo check on Tauri crate)release.yml): triggers onv*tags; matrix of 4 targets — macOS arm64 (.dmg), macOS x64 (.dmg), Windows x64 (.exe/.msi), Linux x64 (.deb/.AppImage); usestauri-apps/tauri-action; creates a draft release; optional code-signing secrets wired inDocumentation
CHANGELOG.mdcreated.github/assets/Testing
Summary by CodeRabbit