Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,37 @@ All notable changes to VouchNet are documented here. This project follows

## Unreleased

### Saved posts and notification inbox
## 1.6.0 - 2026-10-28

- Added saved posts as a real vertical slice: migration `0040_saved_posts`, `/api/saved`, feed Save
controls, and a private `/saved` page that re-checks visibility and block rules on every read.
- Added mark-all-as-read for member notifications at `/api/notifications/read-all`.
- Updated the implementation matrix and authenticated-app docs so messaging and saved content are
no longer described as missing stubs.
### Desktop expansion

- Native Desktop Network: contacts overview, pending invitations (accept/decline), and mutual-context
recommendations with Connect actions via `/api/desktop/network` and `/api/desktop/network/connect`.
- Native Desktop Jobs and Organizations directories backed by `/api/desktop/jobs` with in-app search.
- Native Opportunities rail (`/api/desktop/opportunities`) with remote filter and brief cards.
- Native Saved posts (`/api/desktop/saved`) with unsave, plus command-palette people/jobs/company
search through `/api/desktop/search`.
- Desktop version bumped to **1.6.0** (Windows NSIS/MSI when the release workflow publishes).

### Security hardening

- Production CSP adds `upgrade-insecure-requests` (Experience `srcDoc` iframes remain allowed).
- Permissions-Policy denies payment, USB, Bluetooth, and Topics APIs; API routes emit `nosniff`.
- New `desktopRead` rate-limit tier (60/min) on Desktop search/jobs/opportunities; connection and
saved writes reuse `socialWrite` with device Bearer auth.
- Desktop Tauri CSP tightened (`frame-ancestors 'none'`, `worker-src 'none'`, `script-src 'self'`,
`upgrade-insecure-requests`).

### LinkedIn parity documentation

- Expanded `docs/IA_COMPARISON.md` with a JTBD side-by-side matrix (feed, network, messaging,
notifications, jobs, companies, saved, opportunities, desktop).
- Implementation matrix Desktop row updated for the authenticated native surfaces.

### Earlier unreleased notes folded in

- Saved posts vertical slice (`0040_saved_posts`, `/api/saved`, `/saved`) and notifications
mark-all-read shipped on main ahead of this Desktop expansion.

## 1.5.0 - 2026-10-28

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@vouchnet/desktop",
"private": true,
"version": "1.5.0",
"version": "1.6.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vouchnet-desktop"
version = "1.5.0"
version = "1.6.0"
description = "The installed VouchNet client"
authors = ["VouchNet"]
edition = "2021"
Expand Down
149 changes: 148 additions & 1 deletion apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,37 @@ async fn authenticated_post(
return Err("Your desktop session needs to be reconnected.".into());
}
if !response.status().is_success() {
return Err("VouchNet could not send that message.".into());
return Err("VouchNet could not complete that request.".into());
}
response
.json()
.await
.map_err(|_| "VouchNet returned an invalid response.".into())
}
async fn authenticated_delete(
state: &DesktopState,
path: &str,
payload: serde_json::Value,
) -> Result<serde_json::Value, String> {
let token = access(state).await?;
let response = reqwest::Client::new()
.delete(format!("{}{}", base_url(), path))
.bearer_auth(token)
.json(&payload)
.send()
.await
.map_err(|_| {
"VouchNet is temporarily unavailable. Check your connection and retry.".to_string()
})?;
if response.status().as_u16() == 401 {
*state
.access_token
.lock()
.map_err(|_| "Desktop session lock failed.")? = None;
return Err("Your desktop session needs to be reconnected.".into());
}
if !response.status().is_success() {
return Err("VouchNet could not complete that request.".into());
}
response
.json()
Expand Down Expand Up @@ -374,6 +404,115 @@ async fn desktop_mark_activity_read(
.await
}
#[tauri::command]
async fn desktop_network(state: State<'_, DesktopState>) -> Result<serde_json::Value, String> {
authenticated_get(&state, "/api/desktop/network").await
}
#[tauri::command]
async fn desktop_connect(
state: State<'_, DesktopState>,
payload: serde_json::Value,
) -> Result<serde_json::Value, String> {
authenticated_post(&state, "/api/desktop/network/connect", payload).await
}
fn desktop_query_path(base: &str, pairs: &[(&str, &str)]) -> Result<String, String> {
let mut url = Url::parse(&format!("https://vouchnet.local{base}")).map_err(|e| e.to_string())?;
{
let mut query = url.query_pairs_mut();
for (key, value) in pairs {
query.append_pair(key, value);
}
}
Ok(format!("{}{}", url.path(), url.query().map(|q| format!("?{q}")).unwrap_or_default()))
}
#[tauri::command]
async fn desktop_search(
state: State<'_, DesktopState>,
query: String,
) -> Result<serde_json::Value, String> {
let trimmed = query.trim();
if trimmed.chars().count() > 80 {
return Err("Search query is too long.".into());
}
let path = desktop_query_path("/api/desktop/search", &[("q", trimmed)])?;
authenticated_get(&state, &path).await
}
#[tauri::command]
async fn desktop_jobs(
state: State<'_, DesktopState>,
query: Option<String>,
) -> Result<serde_json::Value, String> {
let trimmed = query.unwrap_or_default();
let trimmed = trimmed.trim();
if trimmed.is_empty() {
return authenticated_get(&state, "/api/desktop/jobs").await;
}
if trimmed.chars().count() > 80 {
return Err("Search query is too long.".into());
}
let path = desktop_query_path("/api/desktop/jobs", &[("q", trimmed)])?;
authenticated_get(&state, &path).await
}
#[tauri::command]
async fn desktop_opportunities(
state: State<'_, DesktopState>,
query: Option<String>,
remote_only: Option<bool>,
) -> Result<serde_json::Value, String> {
let mut pairs: Vec<(&str, String)> = Vec::new();
if let Some(value) = query {
let trimmed = value.trim().to_string();
if !trimmed.is_empty() {
if trimmed.chars().count() > 80 {
return Err("Search query is too long.".into());
}
pairs.push(("q", trimmed));
}
}
if remote_only.unwrap_or(false) {
pairs.push(("remote", "1".into()));
}
if pairs.is_empty() {
return authenticated_get(&state, "/api/desktop/opportunities").await;
}
let borrowed: Vec<(&str, &str)> = pairs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let path = desktop_query_path("/api/desktop/opportunities", &borrowed)?;
authenticated_get(&state, &path).await
}
#[tauri::command]
async fn desktop_saved(state: State<'_, DesktopState>) -> Result<serde_json::Value, String> {
authenticated_get(&state, "/api/desktop/saved").await
}
#[tauri::command]
async fn desktop_save_post(
state: State<'_, DesktopState>,
post_id: String,
) -> Result<serde_json::Value, String> {
if !is_uuid(&post_id) {
return Err("Invalid post.".into());
}
authenticated_post(
&state,
"/api/desktop/saved",
serde_json::json!({ "postId": post_id }),
)
.await
}
#[tauri::command]
async fn desktop_unsave_post(
state: State<'_, DesktopState>,
post_id: String,
) -> Result<serde_json::Value, String> {
if !is_uuid(&post_id) {
return Err("Invalid post.".into());
}
authenticated_delete(
&state,
"/api/desktop/saved",
serde_json::json!({ "postId": post_id }),
)
.await
}
#[tauri::command]
fn desktop_sign_out(state: State<DesktopState>) -> Result<(), String> {
*state
.access_token
Expand All @@ -398,6 +537,14 @@ fn main() {
desktop_send_message,
desktop_activity,
desktop_mark_activity_read,
desktop_network,
desktop_connect,
desktop_search,
desktop_jobs,
desktop_opportunities,
desktop_saved,
desktop_save_post,
desktop_unsave_post,
desktop_sign_out
])
.run(tauri::generate_context!())
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "VouchNet",
"version": "1.5.0",
"version": "1.6.0",
"identifier": "dev.vouchnet.desktop",
"build": {
"beforeDevCommand": "pnpm dev",
Expand All @@ -25,7 +25,7 @@
}
],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://vouchnet.dev; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'"
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://vouchnet.dev; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'; frame-ancestors 'none'; worker-src 'none'; script-src 'self'; upgrade-insecure-requests"
}
},
"bundle": {
Expand Down
Loading
Loading