+29
Cargo.lock
+29
Cargo.lock
···
1644
1644
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
1645
1645
1646
1646
[[package]]
1647
+
name = "gloo-storage"
1648
+
version = "0.3.0"
1649
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1650
+
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
1651
+
dependencies = [
1652
+
"gloo-utils",
1653
+
"js-sys",
1654
+
"serde",
1655
+
"serde_json",
1656
+
"thiserror 1.0.69",
1657
+
"wasm-bindgen",
1658
+
"web-sys",
1659
+
]
1660
+
1661
+
[[package]]
1662
+
name = "gloo-utils"
1663
+
version = "0.2.0"
1664
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1665
+
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
1666
+
dependencies = [
1667
+
"js-sys",
1668
+
"serde",
1669
+
"serde_json",
1670
+
"wasm-bindgen",
1671
+
"web-sys",
1672
+
]
1673
+
1674
+
[[package]]
1647
1675
name = "group"
1648
1676
version = "0.13.0"
1649
1677
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2239
2267
"bytes",
2240
2268
"clap",
2241
2269
"getrandom 0.2.16",
2270
+
"gloo-storage",
2242
2271
"http",
2243
2272
"image",
2244
2273
"jacquard-api",
+4
-13
crates/jacquard-oauth/src/loopback.rs
+4
-13
crates/jacquard-oauth/src/loopback.rs
···
1
1
#![cfg(feature = "loopback")]
2
2
3
3
use crate::{
4
-
atproto::AtprotoClientMetadata,
5
4
authstore::ClientAuthStore,
6
5
client::OAuthClient,
7
6
dpop::DpopExt,
8
7
error::{CallbackError, OAuthError},
9
8
resolver::OAuthResolver,
10
-
scopes::Scope,
11
9
types::{AuthorizeOptions, CallbackParams},
12
10
};
13
11
use jacquard_common::{IntoStatic, cowstr::ToCowStr};
···
122
120
local_addr.port(),
123
121
))
124
122
.unwrap();
125
-
let client_data = crate::session::ClientData {
126
-
keyset: self.registry.client_data.keyset.clone(),
127
-
config: AtprotoClientMetadata::new_localhost(
128
-
Some(vec![redirect.clone()]),
129
-
Some(vec![
130
-
Scope::Atproto,
131
-
Scope::Transition(crate::scopes::TransitionScope::Generic),
132
-
]),
133
-
),
134
-
};
135
123
124
+
let mut client_data = self.registry.client_data.clone();
125
+
// Ensure the redirect URI is set correctly for the loopback server
126
+
client_data.config.redirect_uris = vec![redirect];
136
127
// Build client using store and resolver
137
128
let flow_client = OAuthClient::new_with_shared(
138
129
self.registry.store.clone(),
139
130
self.client.clone(),
140
-
client_data.clone(),
131
+
client_data,
141
132
);
142
133
143
134
// Start auth and get authorization URL
+4
-1
crates/jacquard/Cargo.toml
+4
-1
crates/jacquard/Cargo.toml
···
157
157
jose-jwk = { workspace = true, features = ["p256"] }
158
158
tracing = { workspace = true, optional = true }
159
159
n0-future = { workspace = true, optional = true }
160
-
160
+
gloo-storage = "0.3"
161
161
162
162
[target.'cfg(not(target_family = "wasm"))'.dependencies]
163
163
jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["cache"] }
···
170
170
171
171
[target.'cfg(target_family = "wasm")'.dependencies]
172
172
getrandom = { version = "0.2", features = ["js"] }
173
+
174
+
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
175
+
gloo-storage = "0.3"
173
176
174
177
[dev-dependencies]
175
178
clap.workspace = true
+16
-1
crates/jacquard/src/client.rs
+16
-1
crates/jacquard/src/client.rs
···
16
16
//! - [`token`] - Token storage and persistence
17
17
//! - [`vec_update`] - Trait for fetch-modify-put patterns on array endpoints
18
18
19
+
//pub mod bff_session;
19
20
/// App-password session implementation with auto-refresh
20
21
pub mod credential_session;
21
22
/// Agent error type
···
460
461
/// App password session information from `com.atproto.server.createSession`
461
462
///
462
463
/// Contains the access and refresh tokens along with user identity information.
463
-
#[derive(Debug, Clone)]
464
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
464
465
pub struct AtpSession {
465
466
/// Access token (JWT) used for authenticated requests
467
+
#[serde(borrow)]
466
468
pub access_jwt: CowStr<'static>,
467
469
/// Refresh token (JWT) used to obtain new access tokens
468
470
pub refresh_jwt: CowStr<'static>,
···
470
472
pub did: Did<'static>,
471
473
/// User's handle (e.g., "alice.bsky.social")
472
474
pub handle: Handle<'static>,
475
+
}
476
+
477
+
impl IntoStatic for AtpSession {
478
+
type Output = Self;
479
+
480
+
fn into_static(self) -> Self {
481
+
Self {
482
+
access_jwt: self.access_jwt.into_static(),
483
+
refresh_jwt: self.refresh_jwt.into_static(),
484
+
did: self.did.into_static(),
485
+
handle: self.handle.into_static(),
486
+
}
487
+
}
473
488
}
474
489
475
490
#[cfg(feature = "api")]
+241
crates/jacquard/src/client/bff_session.rs
+241
crates/jacquard/src/client/bff_session.rs
···
1
+
//! Session implementation for front-end clients that proxy to a dedicated backend
2
+
//!
3
+
4
+
//#[cfg(target_arch = "wasm32")]
5
+
use crate::client::SessionStoreError;
6
+
//#[cfg(target_arch = "wasm32")]
7
+
use crate::client::{AtpSession, credential_session::SessionKey};
8
+
//#[cfg(target_arch = "wasm32")]
9
+
use gloo_storage::{LocalStorage, SessionStorage, Storage};
10
+
//#[cfg(target_arch = "wasm32")]
11
+
use jacquard_common::{session::SessionStore, types::string::Did};
12
+
#[cfg(target_arch = "wasm32")]
13
+
use jacquard_oauth::authstore::ClientAuthStore;
14
+
#[cfg(target_arch = "wasm32")]
15
+
use jacquard_oauth::session::{AuthRequestData, ClientSessionData};
16
+
//#[cfg(target_arch = "wasm32")]
17
+
use std::future::Future;
18
+
19
+
//#[cfg(target_arch = "wasm32")]
20
+
#[derive(Clone)]
21
+
pub struct BrowserAuthStore;
22
+
23
+
//#[cfg(target_arch = "wasm32")]
24
+
impl BrowserAuthStore {
25
+
pub fn new() -> Self {
26
+
Self
27
+
}
28
+
29
+
fn session_key(did: &Did<'_>, session_id: &str) -> String {
30
+
format!("session_{}_{}", did.as_ref(), session_id)
31
+
}
32
+
33
+
fn auth_req_key(state: &str) -> String {
34
+
format!("auth_req_{}", state)
35
+
}
36
+
}
37
+
38
+
#[cfg(target_arch = "wasm32")]
39
+
impl ClientAuthStore for BrowserAuthStore {
40
+
fn get_session(
41
+
&self,
42
+
did: &Did<'_>,
43
+
session_id: &str,
44
+
) -> impl Future<Output = Result<Option<ClientSessionData<'_>>, SessionStoreError>> {
45
+
let key = Self::session_key(did, session_id);
46
+
async move {
47
+
match LocalStorage::get::<serde_json::Value>(&key) {
48
+
Ok(value) => {
49
+
let data: ClientSessionData<'static> =
50
+
jacquard::from_json_value::<ClientSessionData>(value).map_err(|e| {
51
+
SessionStoreError::Other(format!("Deserialize error: {}", e).into())
52
+
})?;
53
+
Ok(Some(data))
54
+
}
55
+
Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => Ok(None),
56
+
Err(e) => Err(SessionStoreError::Other(
57
+
format!("LocalStorage error: {}", e).into(),
58
+
)),
59
+
}
60
+
}
61
+
}
62
+
63
+
fn upsert_session(
64
+
&self,
65
+
session: ClientSessionData<'_>,
66
+
) -> impl Future<Output = Result<(), SessionStoreError>> {
67
+
async move {
68
+
use jacquard::IntoStatic;
69
+
70
+
let key = Self::session_key(&session.account_did, &session.session_id);
71
+
let static_session = session.into_static();
72
+
73
+
let value = serde_json::to_value(&static_session)
74
+
.map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?;
75
+
76
+
LocalStorage::set(&key, &value).map_err(|e| {
77
+
SessionStoreError::Other(format!("LocalStorage error: {}", e).into())
78
+
})?;
79
+
80
+
Ok(())
81
+
}
82
+
}
83
+
84
+
fn delete_session(
85
+
&self,
86
+
did: &Did<'_>,
87
+
session_id: &str,
88
+
) -> impl Future<Output = Result<(), SessionStoreError>> {
89
+
let key = Self::session_key(did, session_id);
90
+
async move {
91
+
LocalStorage::delete(&key);
92
+
Ok(())
93
+
}
94
+
}
95
+
96
+
fn get_auth_req_info(
97
+
&self,
98
+
state: &str,
99
+
) -> impl Future<Output = Result<Option<AuthRequestData<'_>>, SessionStoreError>> {
100
+
let key = Self::auth_req_key(state);
101
+
async move {
102
+
match LocalStorage::get::<serde_json::Value>(&key) {
103
+
Ok(value) => {
104
+
let data: AuthRequestData<'static> =
105
+
jacquard::from_json_value::<AuthRequestData>(value).map_err(|e| {
106
+
SessionStoreError::Other(format!("Deserialize error: {}", e).into())
107
+
})?;
108
+
Ok(Some(data))
109
+
}
110
+
Err(gloo_storage::errors::StorageError::KeyNotFound(err)) => {
111
+
tracing::debug!("gloo error: {}", err);
112
+
Ok(None)
113
+
}
114
+
Err(e) => Err(SessionStoreError::Other(
115
+
format!("SessionStorage error: {}", e).into(),
116
+
)),
117
+
}
118
+
}
119
+
}
120
+
121
+
fn save_auth_req_info(
122
+
&self,
123
+
auth_req_info: &AuthRequestData<'_>,
124
+
) -> impl Future<Output = Result<(), SessionStoreError>> {
125
+
async move {
126
+
use jacquard::IntoStatic;
127
+
128
+
let key = Self::auth_req_key(&auth_req_info.state);
129
+
let static_info = auth_req_info.clone().into_static();
130
+
131
+
let value = serde_json::to_value(&static_info)
132
+
.map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?;
133
+
134
+
LocalStorage::set(&key, &value).map_err(|e| {
135
+
SessionStoreError::Other(format!("SessionStorage error: {}", e).into())
136
+
})?;
137
+
138
+
Ok(())
139
+
}
140
+
}
141
+
142
+
fn delete_auth_req_info(
143
+
&self,
144
+
state: &str,
145
+
) -> impl Future<Output = Result<(), SessionStoreError>> {
146
+
let key = Self::auth_req_key(state);
147
+
async move {
148
+
LocalStorage::delete(&key);
149
+
Ok(())
150
+
}
151
+
}
152
+
}
153
+
154
+
#[cfg(target_arch = "wasm32")]
155
+
impl SessionStore<SessionKey, AtpSession> for BrowserAuthStore {
156
+
fn get(&self, key: &SessionKey) -> impl Future<Output = Option<AtpSession>> + Send {
157
+
let key = Self::session_key(&key.0, &key.1);
158
+
async move {
159
+
match LocalStorage::get::<serde_json::Value>(&key) {
160
+
Ok(value) => {
161
+
let data: AtpSession = crate::from_json_value::<AtpSession>(value).ok()?;
162
+
Some(data)
163
+
}
164
+
Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => None,
165
+
Err(_) => None,
166
+
}
167
+
}
168
+
}
169
+
170
+
fn set(
171
+
&self,
172
+
key: SessionKey,
173
+
session: AtpSession,
174
+
) -> impl Future<Output = Result<(), SessionStoreError>> + Send {
175
+
async move {
176
+
let key = Self::session_key(&key.0, &key.1);
177
+
178
+
let value = serde_json::to_value(&session)
179
+
.map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?;
180
+
181
+
LocalStorage::set(&key, &value).map_err(|e| {
182
+
SessionStoreError::Other(format!("LocalStorage error: {}", e).into())
183
+
})?;
184
+
185
+
Ok(())
186
+
}
187
+
}
188
+
189
+
fn del(&self, key: &SessionKey) -> impl Future<Output = Result<(), SessionStoreError>> + Send {
190
+
let key = Self::session_key(&key.0, &key.1);
191
+
async move {
192
+
LocalStorage::delete(&key);
193
+
Ok(())
194
+
}
195
+
}
196
+
}
197
+
198
+
/// This might seem a little silly, and it sort of is, but the intended use here is for proxying requests to another client
199
+
#[cfg(target_arch = "wasm32")]
200
+
impl SessionStore<SessionKey, SessionKey> for BrowserAuthStore {
201
+
fn get(&self, key: &SessionKey) -> impl Future<Output = Option<SessionKey>> + Send {
202
+
let key = Self::session_key(&key.0, &key.1);
203
+
async move {
204
+
match LocalStorage::get::<serde_json::Value>(&key) {
205
+
Ok(value) => {
206
+
let data: SessionKey = crate::from_json_value::<SessionKey>(value).ok()?;
207
+
Some(data)
208
+
}
209
+
Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => None,
210
+
Err(_) => None,
211
+
}
212
+
}
213
+
}
214
+
215
+
fn set(
216
+
&self,
217
+
key: SessionKey,
218
+
session: SessionKey,
219
+
) -> impl Future<Output = Result<(), SessionStoreError>> + Send {
220
+
async move {
221
+
let key = Self::session_key(&key.0, &key.1);
222
+
223
+
let value = serde_json::to_value(&session)
224
+
.map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?;
225
+
226
+
LocalStorage::set(&key, &value).map_err(|e| {
227
+
SessionStoreError::Other(format!("LocalStorage error: {}", e).into())
228
+
})?;
229
+
230
+
Ok(())
231
+
}
232
+
}
233
+
234
+
fn del(&self, key: &SessionKey) -> impl Future<Output = Result<(), SessionStoreError>> + Send {
235
+
let key = Self::session_key(&key.0, &key.1);
236
+
async move {
237
+
LocalStorage::delete(&key);
238
+
Ok(())
239
+
}
240
+
}
241
+
}