Command-line tool for managing your AT Protocol bookmarks. Works with kipclip.com and any app that uses the same record format. kipclip.com
atproto rust kipclip bookmarks tags toread atprotocol
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix OAuth token refresh for long-lived sessions (v0.1.1)

The client_id used during token refresh didn't match the one from login,
causing the PDS to reject refresh requests. Sessions now last up to 2
weeks instead of expiring after ~2 hours.

+40 -6
+7
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## 0.1.1 4 + 5 + ### Fixed 6 + 7 + - Fix OAuth token refresh so sessions last up to 2 weeks instead of expiring after ~2 hours. The client_id used during token refresh now matches the one from login, allowing the PDS to accept refresh token requests. 8 + - Show a clear "Session expired" message with re-login instructions when session restore fails. 9 + 3 10 ## 0.1.0 4 11 5 12 First release of `kip`, the CLI for [kipclip.com](https://kipclip.com).
+2 -1
Cargo.lock
··· 2114 2114 2115 2115 [[package]] 2116 2116 name = "kipclip" 2117 - version = "0.1.0" 2117 + version = "0.1.1" 2118 2118 dependencies = [ 2119 2119 "chrono", 2120 2120 "clap", ··· 2131 2131 "terminal_size", 2132 2132 "tokio", 2133 2133 "unicode-width 0.2.2", 2134 + "url", 2134 2135 ] 2135 2136 2136 2137 [[package]]
+2 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "kipclip" 3 - version = "0.1.0" 3 + version = "0.1.1" 4 4 edition = "2024" 5 5 description = "CLI for kipclip.com – AT Protocol bookmark manager" 6 6 license = "MIT" ··· 25 25 unicode-width = "0.2" 26 26 terminal_size = "0.4" 27 27 jacquard-identity = "0.9" 28 + url = "2" 28 29 regex-lite = "0.1"
+22 -3
src/kipclip/auth.rs
··· 1 1 use jacquard::client::FileAuthStore; 2 + use jacquard::oauth::atproto::AtprotoClientMetadata; 2 3 use jacquard::oauth::client::{OAuthClient, OAuthSession}; 3 4 use jacquard::oauth::loopback::LoopbackConfig; 5 + use jacquard::oauth::session::ClientData; 4 6 use jacquard_identity::JacquardResolver; 5 7 use miette::{IntoDiagnostic, Result, miette}; 6 8 use serde::{Deserialize, Serialize}; 9 + use url::Url; 7 10 8 11 use crate::kipclip::config; 9 12 10 13 /// Concrete session type used throughout the CLI 11 14 pub type Session = OAuthSession<JacquardResolver, FileAuthStore>; 12 15 16 + /// Default loopback port used by jacquard's LoopbackConfig::default() 17 + const LOOPBACK_PORT: u16 = 4000; 18 + 13 19 /// Stored session info (persisted separately from OAuth tokens) 14 20 #[derive(Debug, Clone, Serialize, Deserialize)] 15 21 pub struct SessionInfo { ··· 18 24 pub session_id: String, 19 25 } 20 26 21 - /// Create an OAuth client with file-backed auth store 27 + /// Build client metadata matching what login_with_local_server uses. 28 + /// The client_id URL must include the same redirect_uri and scope params 29 + /// so that token refresh sends the correct client_id to the PDS. 30 + fn loopback_client_metadata() -> AtprotoClientMetadata<'static> { 31 + let redirect = Url::parse(&format!("http://127.0.0.1:{LOOPBACK_PORT}/oauth/callback")).unwrap(); 32 + AtprotoClientMetadata::new_localhost(Some(vec![redirect]), None) 33 + } 34 + 35 + /// Create an OAuth client with file-backed auth store. 36 + /// Uses client metadata that matches the login flow so token refresh works. 22 37 fn oauth_client() -> OAuthClient<JacquardResolver, FileAuthStore> { 23 - OAuthClient::with_default_config(FileAuthStore::new(config::auth_store_path())) 38 + let store = FileAuthStore::new(config::auth_store_path()); 39 + let client_data = ClientData { 40 + keyset: None, 41 + config: loopback_client_metadata(), 42 + }; 43 + OAuthClient::new(store, client_data) 24 44 } 25 45 26 46 /// Login via OAuth loopback flow — opens browser for authorization ··· 36 56 let did_str = did.to_string(); 37 57 let sid_str = session_id.to_string(); 38 58 39 - // Resolve handle via identity 40 59 let info = SessionInfo { 41 60 did: did_str, 42 61 handle: handle.to_string(),
+7 -1
src/main.rs
··· 112 112 113 113 /// Build a PDS client from the stored session 114 114 async fn make_pds_client() -> Result<PdsClient> { 115 - let session = auth::restore_session().await?; 116 115 let info = auth::get_session_info()?; 116 + let session = match auth::restore_session().await { 117 + Ok(s) => s, 118 + Err(_) => { 119 + eprintln!("Session expired. Run: kip login {}", info.handle); 120 + std::process::exit(1); 121 + } 122 + }; 117 123 PdsClient::new(session, &info.did) 118 124 } 119 125